From f3f85e1ab56ec0c082e044d893782bcc465d09b6 Mon Sep 17 00:00:00 2001 From: =?utf8?q?Vladim=C3=ADr=20Vondru=C5=A1?= Date: Sun, 5 May 2019 18:10:49 +0200 Subject: [PATCH] m.sphinx: new plugin for Python docs via external files. Provides a way to document modules, classes and data in the Python doc theme. --- doc/documentation/python.rst | 41 ++++++ doc/plugins.rst | 14 +- doc/plugins/metadata.rst | 2 +- doc/plugins/sphinx.rst | 100 ++++++++++++++ documentation/python.py | 123 ++++++++++++++---- documentation/templates/python/class.html | 3 + documentation/templates/python/module.html | 3 + .../test_python/content/classes.html | 52 ++++++++ .../test_python/content/content.Class.html | 33 +++++ .../test_python/content/content.html | 61 +++++++++ documentation/test_python/content/content.py | 6 + documentation/test_python/content/docs.rst | 14 ++ documentation/test_python/test_content.py | 40 ++++++ plugins/m/sphinx.py | 82 ++++++++++++ plugins/m/test/test_sphinx.py | 25 ++++ site/pelicanconf.py | 16 ++- 16 files changed, 582 insertions(+), 33 deletions(-) create mode 100644 doc/plugins/sphinx.rst create mode 100644 documentation/test_python/content/classes.html create mode 100644 documentation/test_python/content/content.Class.html create mode 100644 documentation/test_python/content/content.html create mode 100644 documentation/test_python/content/content.py create mode 100644 documentation/test_python/content/docs.rst create mode 100644 documentation/test_python/test_content.py create mode 100644 plugins/m/sphinx.py create mode 100644 plugins/m/test/test_sphinx.py diff --git a/doc/documentation/python.rst b/doc/documentation/python.rst index ffe9d39b..f79088be 100644 --- a/doc/documentation/python.rst +++ b/doc/documentation/python.rst @@ -605,8 +605,26 @@ Keyword argument Content :py:`mcss_settings` Dict containing all m.css settings :py:`jinja_environment` Jinja2 environment. Useful for adding new filters etc. +:py:`module_doc_contents` Module documentation contents +:py:`class_doc_contents` Class documentation contents +:py:`data_doc_contents` Data documentation contents =========================== =================================================== +The :py:`module_doc_contents`, :py:`class_doc_contents` and +:py:`data_doc_contents` variables are :py:`Dict[str, Dict[str, str]]`, where +the first level is a name and second level are key/value pairs of the actual +HTML documentation content. Plugins that parse extra documentation inputs (such +as `m.sphinx`_) are supposed to add to the dict, which is then used to fill the +actual documentation contents. The following corresponds to the documentation +source shown in the `External documentation content`_ section below. + +.. code:: py + + class_doc_contents['mymodule.sub.Class'] = { + 'summary': "A pretty class", + 'details': "This class is *pretty*." + } + Registration function for a plugin that needs to query the :py:`OUTPUT` setting might look like this --- the remaining keyword arguments will collapse into the :py:`**kwargs` parameter. See code of various m.css plugins for actual @@ -622,6 +640,29 @@ examples. global output_dir output_dir = mcss_settings['OUTPUT'] +`External documentation content`_ +================================= + +Because it's often not feasible to have the whole documentation stored in +Python docstrings, the generator allows you to supply documentation from +external files. Similarly to `pages`_, the :py:`INPUT_DOCS` setting is a list +of :abbr:`reST ` files that contain documentation for +particular names using custom directives. This is handled by the bundled +`m.sphinx <{filename}/plugins/sphinx.rst>`_ plugin. See its documentation for +detailed description of all features, below is a simple example of using it to +document a class: + +.. code:: py + + PLUGINS = ['m.sphinx'] + +.. code:: rst + + .. py:class:: mymodule.sub.Class + :summary: A pretty class + + This class is *pretty*. + `pybind11 compatibility`_ ========================= diff --git a/doc/plugins.rst b/doc/plugins.rst index b258d04d..4e9ef39f 100644 --- a/doc/plugins.rst +++ b/doc/plugins.rst @@ -57,9 +57,13 @@ below or :gh:`grab the whole Git repository `: :gh:`m.gl `, :gh:`m.vk `, :gh:`m.abbr `, - :gh:`m.filesize `, - :gh:`m.alias ` + :gh:`m.filesize ` +- :gh:`m.alias ` + :label-flat-primary:`pelican only` - :gh:`m.metadata ` + :label-flat-primary:`pelican only` +- :gh:`m.sphinx ` + :label-flat-warning:`python doc only` For the `Python doc theme <{filename}/documentation/python.rst>`_ it's enough to simply list them in :py:`PLUGINS`. For the `Doxygen theme <{filename}/documentation/doxygen.rst>`_, @@ -122,3 +126,9 @@ With the :py:`m.metadata` plugin it's possible to assign additional description and images to authors, categories and tags. The information can then appear on article listing page, as a badge under the article or be added to social meta tags. + +`Sphinx » <{filename}/plugins/sphinx.rst>`_ +=========================================== + +The :py:`m.sphinx` plugin brings Sphinx-compatible directives for documenting +modules, classes and other to the `Python doc theme`_. diff --git a/doc/plugins/metadata.rst b/doc/plugins/metadata.rst index 59d19a8b..b2cb7a88 100644 --- a/doc/plugins/metadata.rst +++ b/doc/plugins/metadata.rst @@ -32,7 +32,7 @@ Metadata .. note-dim:: :class: m-text-center - `« Links and other <{filename}/plugins/links.rst>`_ | `Plugins <{filename}/plugins.rst>`_ + `« Links and other <{filename}/plugins/links.rst>`_ | `Plugins <{filename}/plugins.rst>`_ | `Sphinx » <{filename}/plugins/sphinx.rst>`_ .. role:: html(code) :language: html diff --git a/doc/plugins/sphinx.rst b/doc/plugins/sphinx.rst new file mode 100644 index 00000000..836ccecb --- /dev/null +++ b/doc/plugins/sphinx.rst @@ -0,0 +1,100 @@ +.. + This file is part of m.css. + + Copyright © 2017, 2018, 2019 Vladimír Vondruš + + Permission is hereby granted, free of charge, to any person obtaining a + copy of this software and associated documentation files (the "Software"), + to deal in the Software without restriction, including without limitation + the rights to use, copy, modify, merge, publish, distribute, sublicense, + and/or sell copies of the Software, and to permit persons to whom the + Software is furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included + in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + DEALINGS IN THE SOFTWARE. +.. + +Sphinx +###### + +:breadcrumb: {filename}/plugins.rst Plugins +:footer: + .. note-dim:: + :class: m-text-center + + `« Metadata <{filename}/plugins/metadata.rst>`_ | `Plugins <{filename}/plugins.rst>`_ + +.. role:: html(code) + :language: html +.. role:: py(code) + :language: py +.. role:: rst(code) + :language: rst + +Makes it possible to document APIs with the `Python doc theme <{filename}/documentation/python.rst>`_ +using external files in a way similar to `Sphinx `_. + +.. contents:: + :class: m-block m-default + +`How to use`_ +============= + +`Pelican`_ +---------- + +List the plugin in your :py:`PLUGINS`. + +.. code:: py + + PLUGINS += ['m.sphinx'] + +`Module, class and data docs`_ +============================== + +The :rst:`.. py:module::`, :rst:`.. py:class::` and :rst:`.. py:data::` +directives provide a way to supply module, class and data documentation +content. Directive option is the name to document, directive contents are +the actual contents; in addition the :py:`:summary:` option can override the +docstring extracted using inspection. No restrictions are made on the contents, +it's possible to make use of any additional plugins in the markup. Example: + +.. code:: rst + + .. py:module:: mymodule + :summary: A top-level module. + + This is the top-level module. + + Usage + ----- + + .. code:: pycon + + >>> import mymodule + >>> mymodule.foo() + Hello world! + + .. py:data:: mymodule.ALMOST_PI + :summary: :math:`\pi`, but *less precise*. + +Compared to docstrings, the :py:`:summary:` is interpreted as +:abbr:`reST `, which means you can keep the docstring +formatting simpler (for display inside IDEs or via the builtin :py:`help()`), +while supplying an alternative and more complex-formatted summary for the +actual rendered docs. + +.. note-info:: + + Modules, classes and data described using these directives have to actually + exist (i.e., accessible via inspection) in given module. If given name + doesn't exist, a warning will be printed during processing and the + documentation ignored. diff --git a/documentation/python.py b/documentation/python.py index 3fea6eca..de7b6222 100755 --- a/documentation/python.py +++ b/documentation/python.py @@ -60,6 +60,8 @@ default_config = { 'OUTPUT': 'output', 'INPUT_MODULES': [], 'INPUT_PAGES': [], + 'INPUT_DOCS': [], + 'OUTPUT': 'output', 'THEME_COLOR': '#22272e', 'FAVICON': 'favicon-dark.png', 'STYLESHEETS': [ @@ -118,6 +120,9 @@ class State: self.class_index: List[IndexEntry] = [] self.page_index: List[IndexEntry] = [] self.module_mapping: Dict[str, str] = {} + self.module_docs: Dict[str, Dict[str, str]] = {} + self.class_docs: Dict[str, Dict[str, str]] = {} + self.data_docs: Dict[str, Dict[str, str]] = {} def is_internal_function_name(name: str) -> bool: """If the function name is internal. @@ -247,7 +252,7 @@ def parse_pybind_signature(state: State, signature: str) -> Tuple[str, str, List end = original_signature.find('\n') logging.warning("cannot parse pybind11 function signature %s", original_signature[:end if end != -1 else None]) if end != -1 and len(original_signature) > end + 1 and original_signature[end + 1] == '\n': - summary = extract_summary(original_signature[end + 1:]) + summary = extract_summary({}, [], original_signature[end + 1:]) else: summary = '' return (name, summary, [('…', None, None)], None) @@ -268,13 +273,13 @@ def parse_pybind_signature(state: State, signature: str) -> Tuple[str, str, List end = original_signature.find('\n') logging.warning("cannot parse pybind11 function signature %s", original_signature[:end if end != -1 else None]) if end != -1 and len(original_signature) > end + 1 and original_signature[end + 1] == '\n': - summary = extract_summary(original_signature[end + 1:]) + summary = extract_summary({}, [], original_signature[end + 1:]) else: summary = '' return (name, summary, [('…', None, None)], None) if len(signature) > 1 and signature[1] == '\n': - summary = extract_summary(signature[2:]) + summary = extract_summary({}, [], signature[2:]) else: summary = '' @@ -306,7 +311,12 @@ def parse_pybind_docstring(state: State, name: str, doc: str) -> List[Tuple[str, else: return [parse_pybind_signature(state, doc)] -def extract_summary(doc: str) -> str: +def extract_summary(external_docs, path: List[str], doc: str) -> str: + # Prefer external docs, if available + path_str = '.'.join(path) + if path_str in external_docs: + return render_inline_rst(external_docs[path_str]['summary']) + if not doc: return '' # some modules (xml.etree) have that :( doc = inspect.cleandoc(doc) end = doc.find('\n\n') @@ -344,22 +354,22 @@ def render(config, template: str, page, env: jinja2.Environment): # TODO could keep_trailing_newline fix this better? f.write(b'\n') -def extract_module_doc(path: List[str], module): +def extract_module_doc(state: State, path: List[str], module): assert inspect.ismodule(module) out = Empty() out.url = make_url(path) out.name = path[-1] - out.summary = extract_summary(module.__doc__) + out.summary = extract_summary(state.class_docs, path, module.__doc__) return out -def extract_class_doc(path: List[str], class_): +def extract_class_doc(state: State, path: List[str], class_): assert inspect.isclass(class_) out = Empty() out.url = make_url(path) out.name = path[-1] - out.summary = extract_summary(class_.__doc__) + out.summary = extract_summary(state.class_docs, path, class_.__doc__) return out def extract_enum_doc(state: State, path: List[str], enum_): @@ -375,7 +385,8 @@ def extract_enum_doc(state: State, path: List[str], enum_): if enum_.__doc__ == 'An enumeration.': out.summary = '' else: - out.summary = extract_summary(enum_.__doc__) + # TODO: external summary for enums + out.summary = extract_summary({}, [], enum_.__doc__) out.base = extract_type(enum_.__base__) @@ -388,7 +399,8 @@ def extract_enum_doc(state: State, path: List[str], enum_): if i.__doc__ == enum_.__doc__: value.summary = '' else: - value.summary = extract_summary(i.__doc__) + # TODO: external summary for enum values + value.summary = extract_summary({}, [], i.__doc__) if value.summary: out.has_details = True @@ -399,7 +411,8 @@ def extract_enum_doc(state: State, path: List[str], enum_): elif state.config['PYBIND11_COMPATIBILITY']: assert hasattr(enum_, '__members__') - out.summary = extract_summary(enum_.__doc__) + # TODO: external summary for enums + out.summary = extract_summary({}, [], enum_.__doc__) out.base = None for name, v in enum_.__members__.items(): @@ -431,6 +444,7 @@ def extract_function_doc(state: State, parent, path: List[str], function) -> Lis out.params = [] out.has_complex_params = False out.has_details = False + # TODO: external summary for functions out.summary = summary # Don't show None return type for void functions @@ -508,7 +522,8 @@ def extract_function_doc(state: State, parent, path: List[str], function) -> Lis out.params = [] out.has_complex_params = False out.has_details = False - out.summary = extract_summary(function.__doc__) + # TODO: external summary for functions + out.summary = extract_summary({}, [], function.__doc__) # Decide if classmethod or staticmethod in case this is a method if inspect.isclass(parent): @@ -549,7 +564,8 @@ def extract_property_doc(state: State, path: List[str], property): out = Empty() out.name = path[-1] - out.summary = extract_summary(property.__doc__) + # TODO: external summary for properties + out.summary = extract_summary({}, [], property.__doc__) out.is_settable = property.fset is not None out.is_deletable = property.fdel is not None out.has_details = False @@ -585,6 +601,13 @@ def extract_data_doc(state: State, parent, path: List[str], data): else: out.value = None + # External data summary, if provided + path_str = '.'.join(path) + if path_str in state.data_docs: + # TODO: use also the contents + out.summary = render_inline_rst(state.data_docs[path_str]['summary']) + del state.data_docs[path_str] + return out def render_module(state: State, path, module, env): @@ -597,7 +620,7 @@ def render_module(state: State, path, module, env): breadcrumb += [(i, url_base + 'html')] page = Empty() - page.summary = extract_summary(module.__doc__) + page.summary = extract_summary(state.module_docs, path, module.__doc__) page.url = breadcrumb[-1][1] page.breadcrumb = breadcrumb page.prefix_wbr = '.'.join(path + ['']) @@ -608,6 +631,12 @@ def render_module(state: State, path, module, env): page.data = [] page.has_enum_details = False + # External page content, if provided + path_str = '.'.join(path) + if path_str in state.module_docs: + page.content = render_rst(state.module_docs[path_str]['content']) + state.module_docs[path_str]['used'] = True + # Index entry for this module, returned together with children at the end index_entry = IndexEntry() index_entry.kind = 'module' @@ -657,10 +686,10 @@ def render_module(state: State, path, module, env): # standard lib), but not undocumented classes etc. Render the # submodules and subclasses recursively. if inspect.ismodule(object): - page.modules += [extract_module_doc(subpath, object)] + page.modules += [extract_module_doc(state, subpath, object)] index_entry.children += [render_module(state, subpath, object, env)] elif inspect.isclass(object) and not is_enum(state, object): - page.classes += [extract_class_doc(subpath, object)] + page.classes += [extract_class_doc(state, subpath, object)] index_entry.children += [render_class(state, subpath, object, env)] elif inspect.isclass(object) and is_enum(state, object): enum_ = extract_enum_doc(state, subpath, object) @@ -686,7 +715,7 @@ def render_module(state: State, path, module, env): if is_internal_or_imported_module_member(state, module, path, name, object): continue subpath = path + [name] - page.modules += [extract_module_doc(subpath, object)] + page.modules += [extract_module_doc(state, subpath, object)] index_entry.children += [render_module(state, subpath, object, env)] # Get (and render) inner classes @@ -696,7 +725,7 @@ def render_module(state: State, path, module, env): subpath = path + [name] if not object.__doc__: logging.warning("%s is undocumented", '.'.join(subpath)) - page.classes += [extract_class_doc(subpath, object)] + page.classes += [extract_class_doc(state, subpath, object)] index_entry.children += [render_class(state, subpath, object, env)] # Get enums @@ -791,7 +820,7 @@ def render_class(state: State, path, class_, env): breadcrumb += [(i, url_base + 'html')] page = Empty() - page.summary = extract_summary(class_.__doc__) + page.summary = extract_summary(state.class_docs, path, class_.__doc__) page.url = breadcrumb[-1][1] page.breadcrumb = breadcrumb page.prefix_wbr = '.'.join(path + ['']) @@ -805,6 +834,12 @@ def render_class(state: State, path, class_, env): page.data = [] page.has_enum_details = False + # External page content, if provided + path_str = '.'.join(path) + if path_str in state.class_docs: + page.content = render_rst(state.class_docs[path_str]['content']) + state.class_docs[path_str]['used'] = True + # Index entry for this module, returned together with children at the end index_entry = IndexEntry() index_entry.kind = 'class' @@ -820,7 +855,7 @@ def render_class(state: State, path, class_, env): subpath = path + [name] if not object.__doc__: logging.warning("%s is undocumented", '.'.join(subpath)) - page.classes += [extract_class_doc(subpath, object)] + page.classes += [extract_class_doc(state, subpath, object)] index_entry.children += [render_class(state, subpath, object, env)] # Get enums @@ -877,21 +912,42 @@ def render_class(state: State, path, class_, env): render(state.config, 'class.html', page, env) return index_entry -def publish_rst(source): +def publish_rst(source, translator_class=m.htmlsanity.SaneHtmlTranslator): pub = docutils.core.Publisher( writer=m.htmlsanity.SaneHtmlWriter(), source_class=docutils.io.StringInput, destination_class=docutils.io.StringOutput) pub.set_components('standalone', 'restructuredtext', 'html') - #pub.writer.translator_class = m.htmlsanity.SaneHtmlTranslator + pub.writer.translator_class = translator_class pub.process_programmatic_settings(None, m.htmlsanity.docutils_settings, None) # Docutils uses a deprecated U mode for opening files, so instead of # monkey-patching docutils.io.FileInput to not do that (like Pelican does), # I just read the thing myself. + # TODO *somehow* need to supply the filename to it for better error + # reporting, this is too awful pub.set_source(source=source) pub.publish() return pub +def render_rst(source): + return publish_rst(source).writer.parts.get('body').rstrip() + +class _SaneInlineHtmlTranslator(m.htmlsanity.SaneHtmlTranslator): + # Unconditionally force compact paragraphs. This means the inline HTML + # won't be wrapped in a

which is exactly what we want. + def should_be_compact_paragraph(self, node): + return True + +def render_inline_rst(source): + return publish_rst(source, _SaneInlineHtmlTranslator).writer.parts.get('body').rstrip() + +def render_doc(state: State, filename): + logging.debug("parsing docs from %s", filename) + + # Render the file. The directives should take care of everything, so just + # discard the output afterwards. + with open(filename, 'r') as f: publish_rst(f.read()) + def render_page(state: State, path, filename, env): logging.debug("generating %s.html", '.'.join(path)) @@ -982,7 +1038,17 @@ def run(basedir, config, templates): # Import plugins for plugin in ['m.htmlsanity'] + config['PLUGINS']: module = importlib.import_module(plugin) - module.register_mcss(mcss_settings=config, jinja_environment=env) + module.register_mcss( + mcss_settings=config, + jinja_environment=env, + module_doc_contents=state.module_docs, + class_doc_contents=state.class_docs, + data_doc_contents=state.data_docs) + + # First process the doc input files so we have all data for rendering + # module pages + for file in config['INPUT_DOCS']: + render_doc(state, os.path.join(basedir, file)) for module in config['INPUT_MODULES']: if isinstance(module, str): @@ -993,6 +1059,17 @@ def run(basedir, config, templates): state.class_index += [render_module(state, [module_name], module, env)] + # Warn if there are any unused contents left after processing everything + unused_module_docs = [key for key, value in state.module_docs.items() if not 'used' in value] + unused_class_docs = [key for key, value in state.class_docs.items() if not 'used' in value] + unused_data_docs = [key for key, value in state.data_docs.items() if not 'used' in value] + if unused_module_docs: + logging.warning("The following module doc contents were unused: %s", unused_module_docs) + if unused_class_docs: + logging.warning("The following class doc contents were unused: %s", unused_class_docs) + if unused_data_docs: + logging.warning("The following data doc contents were unused: %s", unused_data_docs) + for page in config['INPUT_PAGES']: state.page_index += render_page(state, [os.path.splitext(os.path.basename(page))[0]], os.path.join(config['INPUT'], page), env) diff --git a/documentation/templates/python/class.html b/documentation/templates/python/class.html index be622826..b26078a3 100644 --- a/documentation/templates/python/class.html +++ b/documentation/templates/python/class.html @@ -52,6 +52,9 @@ + {% endif %} + {% if page.content %} +{{ page.content }} {% endif %} {% if page.classes %}

diff --git a/documentation/templates/python/module.html b/documentation/templates/python/module.html index 440b8f36..9cf77d45 100644 --- a/documentation/templates/python/module.html +++ b/documentation/templates/python/module.html @@ -43,6 +43,9 @@ + {% endif %} + {% if page.content %} +{{ page.content }} {% endif %} {% if page.modules %}
diff --git a/documentation/test_python/content/classes.html b/documentation/test_python/content/classes.html new file mode 100644 index 00000000..9472999b --- /dev/null +++ b/documentation/test_python/content/classes.html @@ -0,0 +1,52 @@ + + + + + My Python Project + + + + + +
+
+
+
+
+

Classes

+
    +
  • + module content This overwrites the docstring for content. +
      +
    • class Class This overwrites the docstring for content.Class.
    • +
    +
  • +
+ +
+
+
+
+ + diff --git a/documentation/test_python/content/content.Class.html b/documentation/test_python/content/content.Class.html new file mode 100644 index 00000000..6643207a --- /dev/null +++ b/documentation/test_python/content/content.Class.html @@ -0,0 +1,33 @@ + + + + + content.Class | My Python Project + + + + + +
+
+
+
+
+

+ content.Class class +

+

This overwrites the docstring for content.Class.

+

This is detailed class docs. Here I also hate how it needs to be +indented.

+
+
+
+
+ + diff --git a/documentation/test_python/content/content.html b/documentation/test_python/content/content.html new file mode 100644 index 00000000..a44c9171 --- /dev/null +++ b/documentation/test_python/content/content.html @@ -0,0 +1,61 @@ + + + + + content | My Python Project + + + + + +
+
+
+
+
+

+ content module +

+

This overwrites the docstring for content.

+
+

Contents

+ +
+

This is detailed module docs. I kinda hate how it needs to be indented, +tho.

+
+

Classes

+
+
class Class
+
This overwrites the docstring for content.Class.
+
+
+
+

Data

+
+
+ CONSTANT: float = 3.14 +
+
This is finally a docstring for content.CONSTANT
+
+
+
+
+
+
+ + diff --git a/documentation/test_python/content/content.py b/documentation/test_python/content/content.py new file mode 100644 index 00000000..ea2d6f1b --- /dev/null +++ b/documentation/test_python/content/content.py @@ -0,0 +1,6 @@ +"""Yes so this is module summary, not shown in the output""" + +class Class: + """And this class summary, not shown either""" + +CONSTANT: float = 3.14 diff --git a/documentation/test_python/content/docs.rst b/documentation/test_python/content/docs.rst new file mode 100644 index 00000000..3c6ceb05 --- /dev/null +++ b/documentation/test_python/content/docs.rst @@ -0,0 +1,14 @@ +.. py:module:: content + :summary: This overwrites the docstring for ``content``. + + This is detailed module docs. I kinda *hate* how it needs to be indented, + tho. + +.. py:class:: content.Class + :summary: This overwrites the docstring for ``content.Class``. + + This is detailed class docs. Here I *also* hate how it needs to be + indented. + +.. py:data:: content.CONSTANT + :summary: This is finally a docstring for ``content.CONSTANT`` diff --git a/documentation/test_python/test_content.py b/documentation/test_python/test_content.py new file mode 100644 index 00000000..bdb77724 --- /dev/null +++ b/documentation/test_python/test_content.py @@ -0,0 +1,40 @@ +# +# This file is part of m.css. +# +# Copyright © 2017, 2018, 2019 Vladimír Vondruš +# +# Permission is hereby granted, free of charge, to any person obtaining a +# copy of this software and associated documentation files (the "Software"), +# to deal in the Software without restriction, including without limitation +# the rights to use, copy, modify, merge, publish, distribute, sublicense, +# and/or sell copies of the Software, and to permit persons to whom the +# Software is furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included +# in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +# DEALINGS IN THE SOFTWARE. +# + +import os + +from . import BaseInspectTestCase + +class Content(BaseInspectTestCase): + def __init__(self, *args, **kwargs): + super().__init__(__file__, '', *args, **kwargs) + + def test(self): + self.run_python({ + 'PLUGINS': ['m.sphinx'], + 'INPUT_DOCS': ['docs.rst'] + }) + self.assertEqual(*self.actual_expected_contents('classes.html')) + self.assertEqual(*self.actual_expected_contents('content.html')) + self.assertEqual(*self.actual_expected_contents('content.Class.html')) diff --git a/plugins/m/sphinx.py b/plugins/m/sphinx.py new file mode 100644 index 00000000..99aece3d --- /dev/null +++ b/plugins/m/sphinx.py @@ -0,0 +1,82 @@ +# +# This file is part of m.css. +# +# Copyright © 2017, 2018, 2019 Vladimír Vondruš +# +# Permission is hereby granted, free of charge, to any person obtaining a +# copy of this software and associated documentation files (the "Software"), +# to deal in the Software without restriction, including without limitation +# the rights to use, copy, modify, merge, publish, distribute, sublicense, +# and/or sell copies of the Software, and to permit persons to whom the +# Software is furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included +# in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +# DEALINGS IN THE SOFTWARE. +# + +from docutils.parsers import rst +from docutils.parsers.rst import directives + +module_doc_output = None +class_doc_output = None +data_doc_output = None + +class PyModule(rst.Directive): + final_argument_whitespace = True + has_content = True + required_arguments = 1 + option_spec = {'summary': directives.unchanged} + + def run(self): + module_doc_output[self.arguments[0]] = { + 'summary': self.options.get('summary', ''), + 'content': '\n'.join(self.content) + } + return [] + +class PyClass(rst.Directive): + final_argument_whitespace = True + has_content = True + required_arguments = 1 + option_spec = {'summary': directives.unchanged} + + def run(self): + class_doc_output[self.arguments[0]] = { + 'summary': self.options.get('summary', ''), + 'content': '\n'.join(self.content) + } + return [] + +class PyData(rst.Directive): + final_argument_whitespace = True + has_content = True + required_arguments = 1 + option_spec = {'summary': directives.unchanged} + + def run(self): + data_doc_output[self.arguments[0]] = { + 'summary': self.options.get('summary', ''), + 'content': '\n'.join(self.content) + } + return [] + +def register_mcss(module_doc_contents, class_doc_contents, data_doc_contents, **kwargs): + global module_doc_output, class_doc_output, data_doc_output + module_doc_output = module_doc_contents + class_doc_output = class_doc_contents + data_doc_output = data_doc_contents + + rst.directives.register_directive('py:module', PyModule) + rst.directives.register_directive('py:class', PyClass) + rst.directives.register_directive('py:data', PyData) + +def register(): # for Pelican + assert not "This plugin is for the m.css Doc theme only" # pragma: no cover diff --git a/plugins/m/test/test_sphinx.py b/plugins/m/test/test_sphinx.py new file mode 100644 index 00000000..5a5e8fa5 --- /dev/null +++ b/plugins/m/test/test_sphinx.py @@ -0,0 +1,25 @@ +# +# This file is part of m.css. +# +# Copyright © 2017, 2018, 2019 Vladimír Vondruš +# +# Permission is hereby granted, free of charge, to any person obtaining a +# copy of this software and associated documentation files (the "Software"), +# to deal in the Software without restriction, including without limitation +# the rights to use, copy, modify, merge, publish, distribute, sublicense, +# and/or sell copies of the Software, and to permit persons to whom the +# Software is furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included +# in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +# DEALINGS IN THE SOFTWARE. +# + +# This module gets tested inside documentation/test_python/. diff --git a/site/pelicanconf.py b/site/pelicanconf.py index 706ad5b6..d7fee31a 100644 --- a/site/pelicanconf.py +++ b/site/pelicanconf.py @@ -80,19 +80,20 @@ M_LINKS_NAVBAR1 = [('Why?', 'why/', 'why', []), ('Themes', 'css/themes/', 'css/themes')]), ('Themes', 'themes/', 'themes', [ ('Writing reST content', 'themes/writing-rst-content/', 'pelican/writing-content'), - ('Pelican', 'themes/pelican/', 'themes/pelican')])] - -M_LINKS_NAVBAR2 = [('Doc generators', 'documentation/', 'documentation', [ + ('Pelican', 'themes/pelican/', 'themes/pelican')]), + ('Doc generators', 'documentation/', 'documentation', [ ('Doxygen C++ theme', 'documentation/doxygen/', 'documentation/doxygen'), - ('Python doc theme', 'documentation/python/', 'documentation/python')]), - ('Plugins', 'plugins/', 'plugins', [ + ('Python doc theme', 'documentation/python/', 'documentation/python')])] + +M_LINKS_NAVBAR2 = [('Plugins', 'plugins/', 'plugins', [ ('HTML sanity', 'plugins/htmlsanity/', 'plugins/htmlsanity'), ('Components', 'plugins/components/', 'plugins/components'), ('Images', 'plugins/images/', 'plugins/images'), ('Math and code', 'plugins/math-and-code/', 'plugins/math-and-code'), ('Links and other', 'plugins/links/', 'plugins/links'), ('Plots and graphs', 'plugins/plots-and-graphs/', 'plugins/plots-and-graphs'), - ('Metadata', 'plugins/metadata/', 'plugins/metadata')]), + ('Metadata', 'plugins/metadata/', 'plugins/metadata'), + ('Sphinx', 'plugins/sphinx/', 'plugins/sphinx')]), ('GitHub', 'https://github.com/mosra/m.css', '', [])] M_LINKS_FOOTER1 = [('m.css', '/'), @@ -125,7 +126,8 @@ M_LINKS_FOOTER4 = [('Plugins', 'plugins/'), ('Math and code', 'plugins/math-and-code/'), ('Plots and graphs', 'plugins/plots-and-graphs/'), ('Links and other', 'plugins/links/'), - ('Metadata', 'plugins/metadata/')] + ('Metadata', 'plugins/metadata/'), + ('Sphinx', 'plugins/sphinx/')] M_FINE_PRINT = """ | m.css. Copyright © `Vladimír Vondruš `_, 2017--2019. Site -- 2.30.2