From: Vladimír Vondruš Date: Fri, 19 Apr 2019 15:47:39 +0000 (+0200) Subject: documentation: initial version of the Python doc generator. X-Git-Url: https://www.chiark.greenend.org.uk/ucgi/~cjwatson/git?a=commitdiff_plain;h=1e7927071f8cb48a011684948061469db34fbe8e;p=blog.git documentation: initial version of the Python doc generator. Inspecting modules, functions, classes, methods and properties, taking the first line of the docstring for a brief docs. Nothing more at the moment, no index or search is hooked up yet. --- diff --git a/doc/build-status.html.in b/doc/build-status.html.in index 3d28d7f6..a3447f51 100644 --- a/doc/build-status.html.in +++ b/doc/build-status.html.in @@ -26,12 +26,12 @@ - Doxygen theme + Documentation themes - Doxygen theme
client search + Documentation themes
client search  
 
diff --git a/doc/documentation.rst b/doc/documentation.rst index 8f7d653f..6c0de820 100644 --- a/doc/documentation.rst +++ b/doc/documentation.rst @@ -78,3 +78,11 @@ it up, reducing the autogenerated clutter, while making it mobile-friendly and extending it with better content layouting capabilities and improved support for C++11 and beyond. Fully compatible with Doxygen URL format and tag files to avoid broken links once you switch. + +`Python docs » <{filename}/documentation/python.rst>`_ +====================================================== + +All features you're used to from either the m.css Pelican theme or the Doxygen +C++ theme, only for Python documentation. Extracting Python APIs using +reflection, *not* parsing Python sources itself. With dedicated support for +pybind11 projects. diff --git a/doc/documentation/doxygen.rst b/doc/documentation/doxygen.rst index b6b36e9a..fb11e35a 100644 --- a/doc/documentation/doxygen.rst +++ b/doc/documentation/doxygen.rst @@ -29,6 +29,11 @@ Doxygen C++ theme :breadcrumb: {filename}/documentation.rst Doc generators :summary: A modern, mobile-friendly drop-in replacement for the stock Doxygen HTML output with a first-class search functionality +:footer: + .. note-dim:: + :class: m-text-center + + `Doc generators <{filename}/documentation.rst>`_ | `Python doc theme » <{filename}/documentation/python.rst>`_ .. role:: cpp(code) :language: cpp @@ -245,7 +250,9 @@ amount of generated content for no added value. ================ The script takes most of the configuration from the ``Doxyfile`` itself, -(ab)using the following builtin options: +(ab)using the following builtin options. The used options are similar to the +`Python config <{filename}python.rst#configuration>`_, but with Doxygen-imposed +naming and constraints. .. class:: m-table m-fullwidth diff --git a/doc/documentation/python.rst b/doc/documentation/python.rst new file mode 100644 index 00000000..d9b20475 --- /dev/null +++ b/doc/documentation/python.rst @@ -0,0 +1,754 @@ +.. + 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. +.. + +Python docs +########### + +:breadcrumb: {filename}/documentation.rst Doc generators +:summary: A modern, mobile-friendly Sphinx-compatible Python documentation + generator with a first-class search functionality +:footer: + .. note-dim:: + :class: m-text-center + + `« Doxygen C++ theme <{filename}/documentation/doxygen.rst>`_ | `Doc generators <{filename}/documentation.rst>`_ + +.. role:: cpp(code) + :language: cpp +.. role:: js(code) + :language: js +.. role:: py(code) + :language: py + +A modern, mobile-friendly Sphinx-compatible Python documentation generator with +a first-class search functionality. Generated by inspecting Python modules and +using either embedded docstrings or external :abbr:`reST ` +files to populate the documentation. + +One of the design goals is providing a similar user experience to the +`Doxygen documentation theme <{filename}doxygen.rst>`_. + +.. note-danger:: Heavily experimental + + This functionality is *heavily* experimental at the moment. It's being used + for the upcoming `Magnum Engine `_ Python bindings + and evolves solely based on that project needs. Note that not everything + listed below is fully implemented yet --- in particular, the search + functionality and :abbr:`reST ` input processing is + currently not yet implemented. + +.. contents:: + :class: m-block m-default + +`Basic usage`_ +============== + +The base is contained in a single Python script and related style/template +files, for advanced features such as math rendering it'll make use of internals +of some `m.css plugins <{filename}/plugins.rst>`_. Clone +:gh:`the m.css GitHub repository ` and look +into the ``documentation/`` directory: + +.. code:: sh + + git clone git://github.com/mosra/m.css + cd m.css/documentation + +The script requires Python 3.6 and depends on `Jinja2 `_ +for templating and docutils for :abbr:`reST ` markup +rendering. You can install the dependencies via ``pip`` or your distribution +package manager, in most cases you'll probably have them already installed: + +.. code:: sh + + # You may need sudo here + pip3 install docutils jinja2 + +Next, you need a configuration file which tells the script what modules to +inspect, how to name the project and where to put the output. In this example, +we'll generate documentation for the Python builtin ``math`` module: + +.. code:: py + + PROJECT_TITLE = "Python math" + INPUT_MODULES = ['math'] + +Now, run the script and pass path to the configuration file to it: + +.. code:: sh + + ./python.py path/to/conf.py + +This will generate an ``output/`` directory next to the ``conf.py`` file and +fill it with the generated output. Open ``index.html`` to see the result. + +`Features`_ +=========== + +- Theme tailored from scratch for Python-specific language features +- Uses code inspection to query modules, classes, data, functions and their + signatures, does not rely on error-prone source code parsing +- Does not force the documentation writer to explicitly list all symbols in + order to have them documented +- Can use both in-code docstrings and external :abbr:`reST ` + files to describe the APIs, giving the user a control over the code size vs + documentation verbosity tradeoff + +`Configuration`_ +================ + +Together with the above :py:`PROJECT_TITLE` and :py:`INPUT_MODULES` variables +mentioned above, the configuration file supports the following variables. The +options are similar to the `Doxygen config <{filename}doxygen.rst#configuration>`_, +but free of the Doxygen-specific naming and constraints. + +.. class:: m-table m-fullwidth + +=================================== =========================================== +Variable Description +=================================== =========================================== +:py:`PROJECT_TITLE: str` Project title. Rendered in top navbar, page + title and fine print. If not set, + :py:`"My Python Project"` is used. +:py:`PROJECT_SUBTITLE: str` Project subtitle. If set, appended in a + thinner font to :py:`PROJECT_TITLE`. +:py:`MAIN_PROJECT_URL: str` If set and :py:`PROJECT_SUBTITLE` is also + set, then :py:`PROJECT_TITLE` in the top + navbar will link to this URL and + :py:`PROJECT_SUBTITLE` to the documentation + main page, similarly as + `shown here <{filename}/css/page-layout.rst#link-back-to-main-site-from-a-subsite>`_. +:py:`INPUT_MODULES: List[Any]` List of modules to generate the docs from. + Values can be either strings or module + objects. +:py:`OUTPUT: str` Where to save the output. Relative paths + relative are to the config file base dir; + if not set, ``output/`` is used. +:py:`THEME_COLOR: str` Color for :html:``, + corresponding to the CSS style. If empty, + no :html:`` tag is rendered. See + `Theme selection`_ for more information. +:py:`FAVICON: str` Favicon URL, used to populate + :html:``. If empty, no + :html:`` tag is rendered. Relative + paths are searched relative to the config + file base dir and to the ``python.py`` + script dir as a fallback. See + `Theme selection`_ for more information. +:py:`STYLESHEETS: List[str]` List of CSS files to include. Relative + paths are searched relative to the config + file base dir and to the ``python.py`` + script dir as a fallback. See `Theme selection`_ + for more information. +:py:`HTML_HEADER: str` HTML code to put at the end of the + :html:`` element. Useful for linking + arbitrary JavaScript code or, for example, + adding :html:`` CSS stylesheets with + additional properties and IDs that are + otherwise not possible with just + :py:`STYLESHEETS`. +:py:`EXTRA_FILES: List[str]` List of extra files to copy (for example + additional CSS files that are :css:`@import`\ ed + from the primary one). Relative paths are + searched relative to the config file base + dir and to the ``python.py`` script dir as + a fallback. +:py:`LINKS_NAVBAR1: List[Any]` Left navbar column links. See + `Navbar links`_ for more information. +:py:`LINKS_NAVBAR2: List[Any]` Right navbar column links. See + `Navbar links`_ for more information. +:py:`PAGE_HEADER: str` :abbr:`reST ` markup to + put at the top of every page. If not set, + nothing is added anywhere. The + ``{filename}`` placeholder is replaced with + current file name. +:py:`FINE_PRINT: str` :abbr:`reST ` markup to + put into the footer. If not set, a default + generic text is used. If empty, no footer + is rendered at all. +:py:`SEARCH_DISABLED: bool` Disable search functionality. If this + option is set, no search data is compiled + and the rendered HTML does not contain + search-related UI or support. If not set, + :py:`False` is used. +:py:`SEARCH_DOWNLOAD_BINARY: bool` Download search data as a binary to save + bandwidth and initial processing time. If + not set, :py:`False` is used. See `Search options`_ + for more information. +:py:`SEARCH_HELP: str` :abbr:`reST ` markup to + display as help text on empty search popup. + If not set, a default message is used. Has + effect only if :py:`SEARCH_DISABLED` is not + :py:`True`. +:py:`SEARCH_BASE_URL: str` Base URL for OpenSearch-based search engine + suggestions for web browsers. See + `Search options`_ for more information. Has + effect only if :py:`SEARCH_DISABLED` is not + :py:`True`. +:py:`SEARCH_EXTERNAL_URL: str` URL for external search. The ``{query}`` + placeholder is replaced with urlencoded + search string. If not set, no external + search is offered. See `Search options`_ + for more information. Has effect only if + :py:`SEARCH_DISABLED` is not :py:`True`. +:py:`DOCUTILS_SETTINGS: Dict[Any]` Additional docutils settings. Key/value + pairs as described in `the docs `_. +=================================== =========================================== + +`Theme selection`_ +------------------ + +By default, the `dark m.css theme <{filename}/css/themes.rst#dark>`_ together +with documentation-theme-specific additions is used, which corresponds to the +following configuration: + +.. code:: py + + STYLESHEETS = [ + 'https://fonts.googleapis.com/css?family=Source+Sans+Pro:400,400i,600,600i%7CSource+Code+Pro:400,400i,600', + '../css/m-dark+documentation.compiled.css'] + THEME_COLOR = '#22272e' + FAVICON = 'favicon-dark.png' + +If you have a site already using the ``m-dark.compiled.css`` file, there's +another file called ``m-dark.documentation.compiled.css``, which contains just +the documentation-theme-specific additions so you can reuse the already cached +``m-dark.compiled.css`` file from your main site: + +.. code:: ini + + STYLESHEETS = [ + 'https://fonts.googleapis.com/css?family=Source+Sans+Pro:400,400i,600,600i%7CSource+Code+Pro:400,400i,600', + '../css/m-dark.compiled.css', + '../css/m-dark.documentation.compiled.css'] + THEME_COLOR = '#22272e' + FAVICON = 'favicon-dark.png' + +If you prefer the `light m.css theme <{filename}/css/themes.rst#light>`_ +instead, use the following configuration (and, similarly, you can use +``m-light.compiled.css`` together with ``m-light.documentation.compiled-css`` +in place of ``m-light+documentation.compiled.css``: + +.. code:: ini + + STYLESHEETS = [ + 'https://fonts.googleapis.com/css?family=Libre+Baskerville:400,400i,700,700i%7CSource+Code+Pro:400,400i,600', + '../css/m-light+documentation.compiled.css'] + THEME_COLOR = '#cb4b16' + FAVICON = 'favicon-light.png' + +See the `CSS files`_ section below for more information about customizing the +CSS files. + +`Navbar links`_ +--------------- + +The :py:`LINKS_NAVBAR1` and :py:`LINKS_NAVBAR2` options define which links are +shown on the top navbar, split into left and right column on small screen +sizes. These options take a list of :py:`(title, path, sub)` tuples --- +``title`` is the link title, ``path`` is path to a particular page or +module/class (in the form of ``module.sub.ClassName``, for example) and ``sub`` +is an optional submenu, containing :py:`(title, path)` tuples. The ``path`` can +be also one of ``pages``, ``modules`` or ``classes``, linking to the page / +module / class index. When rendering, the path is converted to an actual URL to +the destination file. + +By default the variables are defined like following --- there's just three +items in the left column, with no submenus and the right column is empty: + +.. code:: py + + LINKS_NAVBAR1 = [ + ('Pages', 'pages', []), + ('Modules', 'modules', []), + ('Classes', 'classes', [])] + LINKS_NAVBAR2 = [] + +A menu item is highlighted if a page with the same path is the current page. +The ``path`` can be also a full URL --- if it contains a scheme prefix (such as +``https://``), then it's taken as-is, without conversion. + +`Search options`_ +----------------- + +Symbol search is implemented using JavaScript Typed Arrays and does not need +any server-side functionality to perform well --- the client automatically +downloads a tightly packed binary containing search data and performs search +directly on it. + +However, due to `restrictions of Chromium-based browsers `_, +it's not possible to download data using :js:`XMLHttpRequest` when served from +a local file-system. Because of that, the search defaults to producing a +Base85-encoded representation of the search binary and loading that +asynchronously as a plain JavaScript file. This results in the search data +being 25% larger, but since this is for serving from a local filesystem, it's +not considered a problem. If your docs are accessed through a server (or you +don't need Chrome support), enable the :py:`SEARCH_DOWNLOAD_BINARY` option. + +The site can provide search engine metadata using the `OpenSearch `_ +specification. On supported browsers this means you can add the search field to +search engines and search directly from the address bar. To enable search +engine metadata, point :py:`M_SEARCH_BASE_URL` to base URL of your +documentation, for example: + +.. code:: py + + SEARCH_BASE_URL = 'https://doc.magnum.graphics/magnum/' + +In general, even without the above setting, appending ``?q={query}#search`` to +the URL will directly open the search popup with results for ``{query}``. + +.. note-info:: + + OpenSearch also makes it possible to have autocompletion and search results + directly in the browser address bar. However that requires a server-side + search implementation and is not supported at the moment. + +If :py:`SEARCH_EXTERNAL_URL` is specified, full-text search using an external +search engine is offered if nothing is found for given string or if the user +has JavaScript disabled. It's recommended to restrict the search to a +particular domain or add additional keywords to the search query to filter out +irrelevant results. Example, using Google search engine and restricting the +search to a subdomain: + +.. code:: py + + SEARCH_EXTERNAL_URL = 'https://google.com/search?q=site:doc.magnum.graphics+{query}' + +`Content`_ +========== + +By default, if a module contains the :py:`__all__` attribute, all names listed +there are exposed in the documentation. Otherwise, all module (and class) +members are extracted using :py:`inspect.getmembers()`, skipping names +:py:`import`\ ed from elsewhere and underscored names. + +Detecting if a module is a submodule of the current package or if it's +:py:`import`\ ed from elsewhere is tricky, the script thus includes only +submodules that have their :py:`__package__` property the same or one level below +the parent package. If a module's :py:`__package__` is empty, it's assumed to +be a plain module (instead of a package) and since those can't have submodules, +all found submodules in it are ignored. + +.. block-success:: Overriding the set of included names + + In case the autodetection includes more than you want or you need to + include names from other modules as part of the module you need, you can + temporarily override the :py:`__all__` attribute when generating the docs. + For example, the following will list just the :py:`pow()` and :py:`log()` + funtions from the :py:`math` module, ignoring the rest: + + .. code:: py + + import math + math.__all__ = ['pow', 'log'] + + INPUT_MODULES = [math] + +`Docstrings`_ +------------- + +The first paragraph of a module-level, class-level and function-level docstring +is used as a brief documentation, copied as-is to the output without formatting +it in any way. + +.. code:: py + + """Module brief docs""" + + class Foo: + """Class brief docs""" + + def bar(self): + """Function brief docs""" + +.. block-warning:: Limitations + + With the current approach, there are a few limitations: + + - Everything after the first paragraph is ignored (there's no way to have + detailed documentation yet) + - Class and module-level variables can't have a docstring attached due to + how Python works + - Because not every Python API can be documented using docstrings, the + output contains everything, including undocumented names + +`Function and variable annotations`_ +------------------------------------ + +The script uses :py:`inspect.signature()` to query function parameter / return +type annotations together with default values and displays them in the output. +Similar is for module and class variables, extracted from the +:py:`__annotations__` property. If a variable type implements :py:`__repr__()`, +a :py:`repr()` of it is printed as the value, otherwise the value is omitted. + +.. code:: py + + from typing import Tuple, List + + def foo(a: str, be_nice: bool = True) -> Tuple[int, str]: + pass + + SETTINGS: List[Tuple[str, bool]] = [] + +For better readability, if the function signature contains type annotations or +a default value, the arguments are printed each on one line. Otherwise, to +avoid wasting vertical space, the arguments are listed on a single line. + +Similarly to how the builtin :py:`help()` in Python 3.7 started annotating +boundaries between position-only, position-or-keyword and keyword-only +arguments with ``/`` and ``*``, the same is done here --- it's especially +helpful for native functions, where you can for example call :py:`math.sin(0.3)` +but not :py:`math.sin(x=0.3)`, because the ``x`` argument is positional-only. +Currently, positional-only arguments are possible only with native functions, +`PEP570 `_ adds them for pure Python +functions as well. + +In some cases, especially when documenting native functions, the signature +can't be extracted and the function signature shows just an ellipsis (``…``) +instead of the actual argument list. + +`Class methods, static methods, dunder methods, properties`_ +------------------------------------------------------------ + +Methods decorated with :py:`@classmethod` are put into a "Class methods" +section, :py:`@staticmethod`\ s into a "Static methods" section. +Double-underscored methods explicitly implemented in the class are put into a +"Special methods" section, otherwise they're ignored --- by default, Python +adds a large collection of dunder methods to each class and the only way to +know if the method is user-provided or implicit is by checking the docstring. + +.. code:: py + + class MyClass: + @classmethod + def a_classmethod(cls): + """A class method""" + + @staticmethod + def a_staticmethod(): + """A static method""" + + def __init__(self, foo, bar): + """A constructor""" + +Properties added to classes either using the :py:`@property` decorator or +created with the :py:`property()` builtin are added to the "Properties" +section. Each property is annotated with :label-flat-success:`get set del` if +it has a getter, a setter and a :py:`del`\ eter or with :label-flat-warning:`get` +and other variants if it has just some. The docstring and type annotation is +extracted from the property getter. + +.. code:: py + + from typing import Tuple + + class MyClass: + @property + def a_read_write_property(self) -> Tuple[int, int]: + """A read-write tuple property""" + + @a_read_write_property.setter + def a_read_write_property(self, a): + # Docstring and type annotation taken from the getter, no need to + # have it repeated here too + pass + +.. block-warning:: Limitations + + Instance variables added inside :py:`__init__()` are not extracted, as this + would require parsing Python code directly (which is what Sphinx has to do + to support these). + +`Command-line options`_ +======================= + +.. code:: sh + + ./python.py [-h] [--templates TEMPLATES] [--debug] conf + +Arguments: + +- ``conf`` --- configuration file + +Options: + +- ``-h``, ``--help`` --- show this help message and exit +- ``--templates TEMPLATES`` --- template directory. Defaults to the + ``templates/python/`` subdirectory if not set. +- ``--debug`` --- verbose logging output. Useful for debugging. + +`Customizing the template`_ +=========================== + +The rest of the documentation explains how to customize the builtin template to +better suit your needs. Each documentation file is generated from one of the +template files that are bundled with the script. However, it's possible to +provide your own Jinja2 template files for customized experience as well as +modify the CSS styling. + +`CSS files`_ +------------ + +By default, compiled CSS files are used to reduce amount of HTTP requests and +bandwidth needed for viewing the documentation. However, for easier +customization and debugging it's better to use the unprocessed stylesheets. The +:py:`STYLESHEETS` option lists all files that go to the +:html:`` in the resulting HTML markup, while +:py:`EXTRA_FILES` list the indirectly referenced files that need to be copied +to the output as well. Below is an example configuration corresponding to the +dark theme: + +.. code:: py + + STYLESHEETS = [ + 'https://fonts.googleapis.com/css?family=Source+Sans+Pro:400,400i,600,600i%7CSource+Code+Pro:400,400i,600', + '../css/m-dark.css', + '../css/m-documentation.css'] + EXTRA_FILES = [ + '../css/m-grid.css', + '../css/m-components.css', + '../css/pygments-dark.css', + '../css/pygments-console.css'] + THEME_COLOR = '#22272e' + +After making desired changes to the source files, it's possible to postprocess +them back to the compiled version using the ``postprocess.py`` utility as +explained in the `CSS themes <{filename}/css/themes.rst#make-your-own>`_ +documentation. In case of the dark theme, the ``m-dark+documentation.compiled.css`` +and ``m-dark.documentation.compiled.css`` files are produced like this: + +.. code:: sh + + cd css + ./postprocess.py m-dark.css m-documentation.css -o m-dark+documentation.compiled.css + ./postprocess.py m-dark.css m-documentation.css --no-import -o m-dark.documentation.compiled.css + +`Module documentation template`_ +-------------------------------- + +Each output file is rendered with one of these templates: + +.. class:: m-table m-fullwidth + +======================= ======================================================= +Filename Use +======================= ======================================================= +``module.html`` Module documentation. See `Module properties`_ for more + information. +``class.html`` Class documentation. See `Class properties`_ for more + information. +======================= ======================================================= + +Each template gets passed all configuration values from the `Configuration`_ +table as-is, together with a :py:`FILENAME` variable with name of given output +file. In addition to builtin Jinja2 filters, the ``basename_or_url`` filter +returns either a basename of file path, if the path is relative; or a full URL, +if the argument is an absolute URL. It's useful in cases like this: + +.. code:: html+jinja + + {% for css in HTML_EXTRA_STYLESHEET %} + + {% endfor %} + +The actual page contents are provided in a :py:`page` object, which has the +following properties. All exposed data are meant to be passed directly to the +HTML markup without any additional escaping. + +.. class:: m-table m-fullwidth + +======================================= ======================================= +Property Description +======================================= ======================================= +:py:`page.brief` Brief docs +:py:`page.url` File URL +:py:`page.breadcrumb` List of :py:`(title, URL)` tuples for + breadcrumb navigation. +======================================= ======================================= + +Each module page has the following additional properties: + +.. class:: m-table m-fullwidth + +======================================= ======================================= +Property Description +======================================= ======================================= +:py:`page.modules` List of inner modules. See + `Module properties`_ for details. +:py:`page.classes` List of classes. See + `Class properties`_ for details. +:py:`page.functions` List of module-level functions. See + `Function properties`_ for details. +:py:`page.data` List of module-level data. See + `Data properties`_ for details. +======================================= ======================================= + +Each class page has the following additional properties: + +.. class:: m-table m-fullwidth + +======================================= ======================================= +Property Description +======================================= ======================================= +:py:`page.classes` List of classes. See + `Class properties`_ for details. +:py:`page.classmethods` List of class methods (annotated with + :py:`@classmethod`). See + `Function properties`_ for details. +:py:`page.staticmethods` List of static methods (annotated with + :py:`@staticmethod`). See + `Function properties`_ for details. +:py:`page.methods` List of methods. See + `Function properties`_ for details. +:py:`page.dunder_methods` List of double-underscored special + functions. See + `Function properties`_ for details. +:py:`page.properties` List of properties. See + `Property properties`_ for details. +:py:`page.data` List of data. See `Data properties`_ + for details. +======================================= ======================================= + +`Module properties`_ +```````````````````` + +.. class:: m-table m-fullwidth + +======================================= ======================================= +Property Description +======================================= ======================================= +:py:`module.url` URL of detailed module documentation +:py:`module.name` Module name +:py:`module.brief` Brief docs +======================================= ======================================= + +`Class properties`_ +``````````````````` + +.. class:: m-table m-fullwidth + +======================================= ======================================= +Property Description +======================================= ======================================= +:py:`class_.url` URL of detailed class documentation +:py:`class_.name` Class name +:py:`class_.brief` Brief docs +======================================= ======================================= + +`Function properties`_ +`````````````````````` + +.. class:: m-table m-fullwidth + +=================================== =========================================== +Property Description +=================================== =========================================== +:py:`function.name` Function name +:py:`function.brief` Brief docs +:py:`function.type` Function return type annotation [1]_ +:py:`function.params` List of function parameters. See below for + details. +:py:`function.has_complex_params` Set to :py:`True` if the parameter list + should be wrapped on several lines for + better readability (for example when it + contains type annotations or default + arguments). Set to :py:`False` when + wrapping on multiple lines would only + occupy too much vertical space. +:py:`function.has_details` If there is enough content for the full + description block. Currently always set to + :py:`False`. [2]_ +:py:`function.is_classmethod` Set to :py:`True` if the function is + annotated with :py:`@classmethod`, + :py:`False` otherwise. +:py:`function.is_staticmethod` Set to :py:`True` if the function is + annotated with :py:`@staticmethod`, + :py:`False` otherwise. +=================================== =========================================== + +The :py:`func.params` is a list of function parameters and their description. +Each item has the following properties: + +.. class:: m-table m-fullwidth + +=========================== =================================================== +Property Description +=========================== =================================================== +:py:`param.name` Parameter name +:py:`param.type` Parameter type annotation [1]_ +:py:`param.default` Default parameter value, if any +:py:`param.kind` Parameter kind, a string equivalent to one of the + `inspect.Parameter.kind `_ + values +=========================== =================================================== + +In some cases (for example in case of native APIs), the parameters can't be +introspected. In that case, the parameter list is a single entry with ``name`` +set to :py:`"..."` and the rest being empty. + +`Property properties`_ +`````````````````````` + +.. class:: m-table m-fullwidth + +=================================== =========================================== +Property Description +=================================== =========================================== +:py:`property.name` Property name +:py:`property.type` Property getter return type annotation [1]_ +:py:`property.brief` Brief docs +:py:`property.is_writable` If the property is writable +:py:`property.is_deletable` If the property is deletable with :py:`del` +:py:`property.has_details` If there is enough content for the full + description block. Currently always set to + :py:`False`. [2]_ +=================================== =========================================== + +`Data properties`_ +`````````````````` + +.. class:: m-table m-fullwidth + +=================================== =========================================== +Property Description +=================================== =========================================== +:py:`data.name` Data name +:py:`data.type` Data type +:py:`data.brief` Brief docs. Currently always empty. +:py:`data.value` Data value representation +:py:`data.has_details` If there is enough content for the full + description block. Currently always set to + :py:`False`. [2]_ +=================================== =========================================== + +------------------------------- + +.. [1] :py:`i.type` is extracted out of function annotation. If the types + aren't annotated, the annotation is empty. +.. [2] :py:`page.has_*_details` and :py:`i.has_details` are :py:`True` if + there is detailed description, function parameter documentation or + *documented* enum value listing that makes it worth to render the full + description block. If :py:`False`, the member should be included only in + the brief listing on top of the page to avoid unnecessary repetition. diff --git a/documentation/.gitignore b/documentation/.gitignore index 0adb54db..aabcaf98 100644 --- a/documentation/.gitignore +++ b/documentation/.gitignore @@ -5,3 +5,4 @@ test_doxygen/layout_generated_doxyfile/Doxyfile node_modules/ package-lock.json test_doxygen/package-lock.json +test_python/*/output/ diff --git a/documentation/python.py b/documentation/python.py new file mode 100755 index 00000000..4d81ae1b --- /dev/null +++ b/documentation/python.py @@ -0,0 +1,542 @@ +#!/usr/bin/env python3 + +# +# 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 argparse +import copy +import urllib.parse +import html +import importlib +import inspect +import logging +import mimetypes +import os +import sys +import shutil + +from types import SimpleNamespace as Empty +from importlib.machinery import SourceFileLoader +from typing import Tuple, Dict, Any, List +from urllib.parse import urljoin + +import jinja2 + +sys.path.append(os.path.join(os.path.dirname(os.path.realpath(__file__)), '../plugins')) +import m.htmlsanity + +default_templates = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'templates/python/') + +default_config = { + 'PROJECT_TITLE': 'My Python Project', + 'PROJECT_SUBTITLE': None, + 'MAIN_PROJECT_URL': None, + 'INPUT_MODULES': [], + 'OUTPUT': 'output', + 'THEME_COLOR': '#22272e', + 'FAVICON': 'favicon-dark.png', + 'STYLESHEETS': [ + 'https://fonts.googleapis.com/css?family=Source+Sans+Pro:400,400i,600,600i%7CSource+Code+Pro:400,400i,600', + '../css/m-dark+documentation.compiled.css'], + 'EXTRA_FILES': [], + 'LINKS_NAVBAR1': [ + ('Pages', 'pages', []), + ('Modules', 'modules', []), + ('Classes', 'classes', [])], + 'LINKS_NAVBAR2': [], + 'PAGE_HEADER': None, + 'FINE_PRINT': '[default]', + 'SEARCH_DISABLED': False, + 'SEARCH_DOWNLOAD_BINARY': False, + 'SEARCH_HELP': """.. raw:: html + +

Search for modules, classes, functions and other + symbols. You can omit any prefix from the symbol path; adding a . + suffix lists all members of given symbol.

+

Use + / to navigate through the list, + Enter to go. + Tab autocompletes common prefix, you can + copy a link to the result using ⌘ + L while ⌘ + M produces a Markdown link.

+""", + 'SEARCH_BASE_URL': None, + 'SEARCH_EXTERNAL_URL': None, +} + +def is_internal_function_name(name: str) -> bool: + """If the function name is internal. + + Skips underscored functions but keeps special functions such as __init__. + """ + return name.startswith('_') and not (name.startswith('__') and name.endswith('__')) + +def is_internal_or_imported_module_member(parent, path: str, name: str, object) -> bool: + """If the module member is internal or imported.""" + + if name.startswith('_'): return True + + # If this is not a module, check if the enclosing module of the object is + # what expected. If not, it's a class/function/... imported from elsewhere + # and we don't want those. + # TODO: xml.dom.domreg says the things from it should be imported as + # xml.dom.foo() and this check discards them, can it be done without + # manually adding __all__? + if not inspect.ismodule(object): + # Variables don't have the __module__ attribute, so check for its + # presence. Right now *any* variable will be present in the output, as + # there is no way to check where it comes from. + if hasattr(object, '__module__') and object.__module__ != '.'.join(path): + return True + + # If this is a module, then things get complicated again and we need to + # handle modules and packages differently. See also for more info: + # https://stackoverflow.com/a/7948672 + else: + # The parent is a single-file module (not a package), these don't have + # submodules so this is most definitely an imported module. Source: + # https://docs.python.org/3/reference/import.html#packages + if not parent.__package__: return True + + # The parent is a package and this is either a submodule or a + # subpackage. Check that the __package__ of parent and child is either + # the same or it's parent + child name + if object.__package__ not in [parent.__package__, parent.__package__ + '.' + name]: return True + + # If nothing of the above matched, then it's a thing we want to document + return False + +def make_url(path: List[str]) -> str: + return '.'.join(path) + '.html' + +def extract_brief(doc: str) -> str: + if not doc: return '' # some modules (xml.etree) have that :( + doc = inspect.cleandoc(doc) + end = doc.find('\n\n') + return html.escape(doc if end == -1 else doc[:end]) + +def extract_type(type) -> str: + # For types we concatenate the type name with its module unless it's + # builtins (i.e., we want re.Match but not builtins.int). + return (type.__module__ + '.' if type.__module__ != 'builtins' else '') + type.__name__ + +def extract_annotation(annotation) -> str: + # TODO: why this is not None directly? + if annotation is inspect.Signature.empty: return None + + # To avoid getting for classes (and getting foo.bar + # instead) but getting the actual type for types annotated with e.g. + # List[int], we need to branch on isclass() + if inspect.isclass(annotation): return extract_type(annotation) + return str(annotation) + +def render(config, template: str, page, env: jinja2.Environment): + template = env.get_template(template) + rendered = template.render(page=page, FILENAME=page.url, **config) + with open(os.path.join(config['OUTPUT'], page.url), 'wb') as f: + f.write(rendered.encode('utf-8')) + # Add back a trailing newline so we don't need to bother with + # patching test files to include a trailing newline to make Git + # happy + # TODO could keep_trailing_newline fix this better? + f.write(b'\n') + +def extract_module_doc(path: List[str], module): + assert inspect.ismodule(module) + + out = Empty() + out.url = make_url(path) + out.name = path[-1] + out.brief = extract_brief(module.__doc__) + return out + +def extract_class_doc(path: List[str], class_): + assert inspect.isclass(class_) + + out = Empty() + out.url = make_url(path) + out.name = path[-1] + out.brief = extract_brief(class_.__doc__) + return out + +def extract_function_doc(path: List[str], function): + assert inspect.isfunction(function) or inspect.ismethod(function) or inspect.isroutine(function) + + out = Empty() + out.name = path[-1] + out.brief = extract_brief(function.__doc__) + out.params = [] + out.has_complex_params = False + out.has_details = False + + try: + signature = inspect.signature(function) + out.type = extract_annotation(signature.return_annotation) + for i in signature.parameters.values(): + param = Empty() + param.name = i.name + param.type = extract_annotation(i.annotation) + if param.type: + out.has_complex_params = True + if i.default is inspect.Signature.empty: + param.default = None + else: + param.default = repr(i.default) + out.has_complex_params = True + param.kind = str(i.kind) + out.params += [param] + + # In CPython, some builtin functions (such as math.log) do not provide + # metadata about their arguments. Source: + # https://docs.python.org/3/library/inspect.html#inspect.signature + except ValueError: + param = Empty() + param.name = '...' + param.name_type = param.name + out.params = [param] + + return out + +def extract_property_doc(path: List[str], property): + assert inspect.isdatadescriptor(property) + + out = Empty() + out.name = path[-1] + out.brief = extract_brief(property.__doc__) + out.is_settable = property.fset is not None + out.is_deletable = property.fdel is not None + out.has_details = False + + try: + signature = inspect.signature(property.fget) + out.type = extract_annotation(signature.return_annotation) + except ValueError: + out.type = None + + return out + +def extract_data_doc(parent, path: List[str], data): + assert not inspect.ismodule(data) and not inspect.isclass(data) and not inspect.isroutine(data) and not inspect.isframe(data) and not inspect.istraceback(data) and not inspect.iscode(data) + + out = Empty() + out.name = path[-1] + # Welp. https://stackoverflow.com/questions/8820276/docstring-for-variable + out.brief = '' + out.has_details = False + if hasattr(parent, '__annotations__') and out.name in parent.__annotations__: + out.type = extract_annotation(parent.__annotations__[out.name]) + else: + out.type = None + # The autogenerated is useless, so provide the value + # only if __repr__ is implemented for given type + if '__repr__' in type(data).__dict__: + out.value = html.escape(repr(data)) + else: + out.value = None + + return out + +def render_module(config, path, module, env): + logging.debug("generating %s.html", '.'.join(path)) + + url_base = '' + breadcrumb = [] + for i in path: + url_base += i + '.' + breadcrumb += [(i, url_base + 'html')] + + page = Empty() + page.brief = extract_brief(module.__doc__) + page.url = breadcrumb[-1][1] + page.breadcrumb = breadcrumb + page.modules = [] + page.classes = [] + page.functions = [] + page.data = [] + + # This is actually complicated -- if the module defines __all__, use that. + # The __all__ is meant to expose the public API, so we don't filter out + # underscored things. + if hasattr(module, '__all__'): + for name in module.__all__: + # Everything available in __all__ is already imported, so get those + # directly + object = getattr(module, name) + subpath = path + [name] + + # We allow undocumented submodules (since they're often in the + # standard lib), but not undocumented classes etc. Render the + # submodules and subclasses recursively. + if inspect.ismodule(object): + page.modules += [extract_module_doc(subpath, object)] + render_module(config, subpath, object, env) + elif inspect.isclass(object): + page.classes += [extract_class_doc(subpath, object)] + render_class(config, subpath, object, env) + elif inspect.isfunction(object) or inspect.isbuiltin(object): + page.functions += [extract_function_doc(subpath, object)] + # Assume everything else is data. The builtin help help() (from + # pydoc) does the same: + # https://github.com/python/cpython/blob/d29b3dd9227cfc4a23f77e99d62e20e063272de1/Lib/pydoc.py#L113 + # TODO: unify this query + elif not inspect.isframe(object) and not inspect.istraceback(object) and not inspect.iscode(object): + page.data += [extract_data_doc(module, subpath, object)] + else: # pragma: no cover + logging.warning("unknown symbol %s in %s", name, '.'.join(path)) + + # Otherwise, enumerate the members using inspect. However, inspect lists + # also imported modules, functions and classes, so take only those which + # have __module__ equivalent to `path`. + else: + # Get (and render) inner modules + for name, object in inspect.getmembers(module, inspect.ismodule): + if is_internal_or_imported_module_member(module, path, name, object): continue + + subpath = path + [name] + page.modules += [extract_module_doc(subpath, object)] + render_module(config, subpath, object, env) + + # Get (and render) inner classes + for name, object in inspect.getmembers(module, inspect.isclass): + if is_internal_or_imported_module_member(module, path, name, object): continue + + subpath = path + [name] + if not object.__doc__: logging.warning("%s is undocumented", '.'.join(subpath)) + + page.classes += [extract_class_doc(subpath, object)] + render_class(config, subpath, object, env) + + # Get inner functions + for name, object in inspect.getmembers(module, lambda o: inspect.isfunction(o) or inspect.isbuiltin(o)): + if is_internal_or_imported_module_member(module, path, name, object): continue + + subpath = path + [name] + if not object.__doc__: logging.warning("%s() is undocumented", '.'.join(subpath)) + + page.functions += [extract_function_doc(subpath, object)] + + # Get data + # TODO: unify this query + for name, object in inspect.getmembers(module, lambda o: not inspect.ismodule(o) and not inspect.isclass(o) and not inspect.isroutine(o) and not inspect.isframe(o) and not inspect.istraceback(o) and not inspect.iscode(o)): + if is_internal_or_imported_module_member(module, path, name, object): continue + + page.data += [extract_data_doc(module, path + [name], object)] + + render(config, 'module.html', page, env) + +# Builtin dunder functions have hardcoded docstrings. This is totally useless +# to have in the docs, so filter them out. Uh... kinda ugly. +_filtered_builtin_functions = set([ + ('__delattr__', "Implement delattr(self, name)."), + ('__dir__', "Default dir() implementation."), + ('__eq__', "Return self==value."), + ('__format__', "Default object formatter."), + ('__ge__', "Return self>=value."), + ('__getattribute__', "Return getattr(self, name)."), + ('__gt__', "Return self>value."), + ('__hash__', "Return hash(self)."), + ('__init__', "Initialize self. See help(type(self)) for accurate signature."), + ('__init_subclass__', + "This method is called when a class is subclassed.\n\n" + "The default implementation does nothing. It may be\n" + "overridden to extend subclasses.\n"), + ('__le__', "Return self<=value."), + ('__lt__', "Return self + + + + {% block title %}{{ PROJECT_TITLE }}{% if PROJECT_SUBTITLE %} {{ PROJECT_SUBTITLE }}{% endif %}{% endblock %} + {% for css in STYLESHEETS %} + + {% endfor %} + {% if FAVICON %} + + {% endif %} + {% if not SEARCH_DISABLED and SEARCH_BASE_URL %} + + {% endif %} + {% block header_links %} + {% endblock %} + + {% if THEME_COLOR %} + + {% endif %} + {% if HTML_HEADER %} + {{ HTML_HEADER|indent(2) }} + {% endif %} + + +
+
+
+
+
+ {% if PAGE_HEADER %} + {{ PAGE_HEADER|render_rst|replace('{filename}', FILENAME) }} + {% endif %} +{% block main %} +{% endblock %} +
+
+
+
+{% if not SEARCH_DISABLED %} + + +{% if SEARCH_DOWNLOAD_BINARY %} + +{% else %} + +{% endif %} +{% endif %} +{% if FINE_PRINT %} +
+{% endif %} + + diff --git a/documentation/templates/python/class.html b/documentation/templates/python/class.html new file mode 100644 index 00000000..27d15f6b --- /dev/null +++ b/documentation/templates/python/class.html @@ -0,0 +1,120 @@ +{% extends 'base.html' %} + +{% macro entry_class(class) %}{% include 'entry-class.html' %}{% endmacro %} +{% macro entry_function(function) %}{% include 'entry-function.html' %}{% endmacro %} +{% macro entry_property(property) %}{% include 'entry-property.html' %}{% endmacro %} +{% macro entry_data(data) %}{% include 'entry-data.html' %}{% endmacro %} + +{% block title %}{% set j = joiner('.') %}{% for name, _ in page.breadcrumb %}{{ j() }}{{ name }}{% endfor %} | {{ super() }}{% endblock %} + +{% block main %} +

+ {%+ for name, target in page.breadcrumb[:-1] %}{{ name }}.{% endfor %}{{ page.breadcrumb[-1][0] }} class +

+ {% if page.brief %} +

{{ page.brief }}

+ {% endif %} + {% if page.classes or page.classmethods or page.staticmethods or page.methods or page.dunder_methods or page.properties or page.data %} +
+

Contents

+
    +
  • + Reference +
      + {% if page.classes %} +
    • Classes
    • + {% endif %} + {% if page.classmethods %} +
    • Class methods
    • + {% endif %} + {% if page.staticmethods %} +
    • Static methods
    • + {% endif %} + {% if page.methods %} +
    • Methods
    • + {% endif %} + {% if page.dunder_methods %} +
    • Special methods
    • + {% endif %} + {% if page.properties %} +
    • Properties
    • + {% endif %} + {% if page.data %} +
    • Data
    • + {% endif %} +
    +
  • +
+
+ {% endif %} + {% if page.classes %} +
+

Classes

+
+ {% for class in page.classes %} +{{ entry_class(class) }} + {% endfor %} +
+
+ {% endif %} + {% if page.classmethods %} +
+

Class methods

+
+ {% for function in page.classmethods %} +{{ entry_function(function) }} + {% endfor %} +
+
+ {% endif %} + {% if page.staticmethods %} +
+

Static methods

+
+ {% for function in page.staticmethods %} +{{ entry_function(function) }} + {% endfor %} +
+
+ {% endif %} + {% if page.methods %} +
+

Methods

+
+ {% for function in page.methods %} +{{ entry_function(function) }} + {% endfor %} +
+
+ {% endif %} + {% if page.dunder_methods %} +
+

Special methods

+
+ {% for function in page.dunder_methods %} +{{ entry_function(function) }} + {% endfor %} +
+
+ {% endif %} + {% if page.properties %} +
+

Properties

+
+ {% for property in page.properties %} +{{ entry_property(property) }} + {% endfor %} +
+
+ {% endif %} + {% if page.data %} +
+

Data

+
+ {% for data in page.data %} +{{ entry_data(data) }} + {% endfor %} +
+
+ {% endif %} +{% endblock %} diff --git a/documentation/templates/python/entry-class.html b/documentation/templates/python/entry-class.html new file mode 100644 index 00000000..a294f489 --- /dev/null +++ b/documentation/templates/python/entry-class.html @@ -0,0 +1,2 @@ +
class {{ class.name }}{% if class.is_deprecated %} deprecated{% endif %}
+
{{ class.brief }}
diff --git a/documentation/templates/python/entry-data.html b/documentation/templates/python/entry-data.html new file mode 100644 index 00000000..d4e4d881 --- /dev/null +++ b/documentation/templates/python/entry-data.html @@ -0,0 +1,6 @@ +
+ {{ data.name }}{% if data.type %}: {{ data.type }}{% endif %}{% if data.value %} = {{ data.value }}{% endif %} + {# This has to be here to avoid the newline being eaten #} + +
+
{{ data.brief }}
diff --git a/documentation/templates/python/entry-function.html b/documentation/templates/python/entry-function.html new file mode 100644 index 00000000..c5f43ab3 --- /dev/null +++ b/documentation/templates/python/entry-function.html @@ -0,0 +1,5 @@ +
+ {% set j = joiner('\n ' if function.has_complex_params else ' ') %} + def {{ function.name }}({% for param in function.params %}{% if loop.index0 %}{% if function.params[loop.index0 - 1].kind == 'POSITIONAL_OR_KEYWORD' and param.kind == 'KEYWORD_ONLY' %}, *,{% else %},{% endif %}{% endif %}{{ j() }}{% if param.kind == 'VAR_POSITIONAL' %}*{% elif param.kind == 'VAR_KEYWORD' %}**{% endif %}{{ param.name }}{% if param.type %}: {{ param.type }}{% endif %}{% if param.default %} = {{ param.default }}{% endif %}{% if param.kind == 'POSITIONAL_ONLY' and (loop.last or function.params[loop.index0 + 1].kind != 'POSITIONAL_ONLY') %}, /{% endif %}{% endfor %}){% if function.type %} -> {{ function.type }}{% endif %} +
+
{{ function.brief }}
diff --git a/documentation/templates/python/entry-module.html b/documentation/templates/python/entry-module.html new file mode 100644 index 00000000..504066de --- /dev/null +++ b/documentation/templates/python/entry-module.html @@ -0,0 +1,2 @@ +
module {{ module.name }}{% if module.is_deprecated %} deprecated{% endif %}
+
{{ module.brief }}
diff --git a/documentation/templates/python/entry-property.html b/documentation/templates/python/entry-property.html new file mode 100644 index 00000000..8abe1146 --- /dev/null +++ b/documentation/templates/python/entry-property.html @@ -0,0 +1,4 @@ +
+ {{ property.name }}{% if property.type %}: {{ property.type }}{% endif %} get{% if property.is_settable %} set{% endif %}{% if property.is_deletable %} del{% endif %} +
+
{{ property.brief }}
diff --git a/documentation/templates/python/module.html b/documentation/templates/python/module.html new file mode 100644 index 00000000..09eada78 --- /dev/null +++ b/documentation/templates/python/module.html @@ -0,0 +1,81 @@ +{% extends 'base.html' %} + +{% macro entry_module(module) %}{% include 'entry-module.html' %}{% endmacro %} +{% macro entry_class(class) %}{% include 'entry-class.html' %}{% endmacro %} +{% macro entry_function(function) %}{% include 'entry-function.html' %}{% endmacro %} +{% macro entry_data(data) %}{% include 'entry-data.html' %}{% endmacro %} + +{% block title %}{% set j = joiner('.') %}{% for name, _ in page.breadcrumb %}{{ j() }}{{ name }}{% endfor %} | {{ super() }}{% endblock %} + +{% block main %} +

+ {%+ for name, target in page.breadcrumb[:-1] %}{{ name }}.{% endfor %}{{ page.breadcrumb[-1][0] }} module +

+ {% if page.brief %} +

{{ page.brief }}

+ {% endif %} + {% if page.modules or page.classes or page.functions or page.data %} +
+

Contents

+
    +
  • + Reference +
      + {% if page.modules %} +
    • Modules
    • + {% endif %} + {% if page.classes %} +
    • Classes
    • + {% endif %} + {% if page.functions %} +
    • Functions
    • + {% endif %} + {% if page.data %} +
    • Data
    • + {% endif %} +
    +
  • +
+
+ {% endif %} + {% if page.modules %} +
+

Modules

+
+ {% for module in page.modules %} +{{ entry_module(module) }} + {% endfor %} +
+
+ {% endif %} + {% if page.classes %} +
+

Classes

+
+ {% for class in page.classes %} +{{ entry_class(class) }} + {% endfor %} +
+
+ {% endif %} + {% if page.functions %} +
+

Functions

+
+ {% for function in page.functions %} +{{ entry_function(function) }} + {% endfor %} +
+
+ {% endif %} + {% if page.data %} +
+

Data

+
+ {% for data in page.data %} +{{ entry_data(data) }} + {% endfor %} +
+
+ {% endif %} +{% endblock %} diff --git a/documentation/templates/python/page.html b/documentation/templates/python/page.html new file mode 100644 index 00000000..41bc6970 --- /dev/null +++ b/documentation/templates/python/page.html @@ -0,0 +1,9 @@ +{% extends 'base.html' %} + +{% block title %}{% set j = joiner('.') %}{% for name, _ in page.breadcrumb %}{{ j() }}{{ name }}{% endfor %} | {{ super() }}{% endblock %} + +{% block main %} +

+ {%+ for name, target in page.breadcrumb[:-1] %}{{ name }}.{% endfor %}{{ page.breadcrumb[-1][0] }} +

+{% endblock %} diff --git a/documentation/test_python/__init__.py b/documentation/test_python/__init__.py new file mode 100644 index 00000000..1c9a7379 --- /dev/null +++ b/documentation/test_python/__init__.py @@ -0,0 +1,79 @@ +# +# 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 copy +import sys +import os +import inspect +import shutil +import unittest + +from python import run, default_templates, default_config + +class BaseTestCase(unittest.TestCase): + def __init__(self, path, dir, *args, **kwargs): + unittest.TestCase.__init__(self, *args, **kwargs) + # Full directory name (for test_something.py the directory is something_dir{} + self.dirname = os.path.splitext(os.path.basename(path))[0][5:] + ('_' + dir if dir else '') + # Absolute path to this directory + self.path = os.path.join(os.path.dirname(os.path.realpath(path)), self.dirname) + + # Display ALL THE DIFFS + self.maxDiff = None + + def setUp(self): + if os.path.exists(os.path.join(self.path, 'output')): shutil.rmtree(os.path.join(self.path, 'output')) + + def run_python(self, config_overrides={}, templates=default_templates): + # Defaults that make sense for the tests + config = copy.deepcopy(default_config) + config.update({ + 'FINE_PRINT': None, + 'THEME_COLOR': None, + 'FAVICON': None, + 'LINKS_NAVBAR1': None, + 'LINKS_NAVBAR2': None, + # None instead of [] so we can detect even an empty override + 'INPUT_MODULES': None, + 'SEARCH_DISABLED': True, + 'OUTPUT': os.path.join(self.path, 'output') + }) + + # Update it with config overrides, specify the input module if not + # already + config.update(config_overrides) + if config['INPUT_MODULES'] is None: + sys.path.append(self.path) + config['INPUT_MODULES'] = [self.dirname] + + run(self.path, config, templates=templates) + + def actual_expected_contents(self, actual, expected = None): + if not expected: expected = actual + + with open(os.path.join(self.path, expected)) as f: + expected_contents = f.read().strip() + with open(os.path.join(self.path, 'output', actual)) as f: + actual_contents = f.read().strip() + return actual_contents, expected_contents diff --git a/documentation/test_python/inspect_all_property/inspect_all_property.html b/documentation/test_python/inspect_all_property/inspect_all_property.html new file mode 100644 index 00000000..771b037c --- /dev/null +++ b/documentation/test_python/inspect_all_property/inspect_all_property.html @@ -0,0 +1,76 @@ + + + + + inspect_all_property | My Python Project + + + + + +
+
+ + diff --git a/documentation/test_python/inspect_all_property/inspect_all_property/__init__.py b/documentation/test_python/inspect_all_property/inspect_all_property/__init__.py new file mode 100644 index 00000000..cd670b64 --- /dev/null +++ b/documentation/test_python/inspect_all_property/inspect_all_property/__init__.py @@ -0,0 +1,24 @@ +from . import hidden +from . import _private_but_exposed + +class HiddenClass: + """A documented class not exposed in __all__""" + pass + +class _PrivateButExposedClass: + # An undocumented class but exposed to + pass + +def hidden_func(a, b, c): + """A documented function not exposed in __all__""" + pass + +def _private_but_exposed_func(): + # A private thing but exposed in __all__ + pass + +hidden_data = 34 + +_private_data = 'Hey!' + +__all__ = ['_private_but_exposed', '_PrivateButExposedClass', '_private_but_exposed_func', '_private_data'] diff --git a/documentation/test_python/inspect_all_property/inspect_all_property/_private_but_exposed.py b/documentation/test_python/inspect_all_property/inspect_all_property/_private_but_exposed.py new file mode 100644 index 00000000..e69de29b diff --git a/documentation/test_python/inspect_all_property/inspect_all_property/hidden.py b/documentation/test_python/inspect_all_property/inspect_all_property/hidden.py new file mode 100644 index 00000000..e69de29b diff --git a/documentation/test_python/inspect_annotations/inspect_annotations.Foo.html b/documentation/test_python/inspect_annotations/inspect_annotations.Foo.html new file mode 100644 index 00000000..6954a785 --- /dev/null +++ b/documentation/test_python/inspect_annotations/inspect_annotations.Foo.html @@ -0,0 +1,51 @@ + + + + + inspect_annotations.Foo | My Python Project + + + + + +
+
+ + diff --git a/documentation/test_python/inspect_annotations/inspect_annotations.html b/documentation/test_python/inspect_annotations/inspect_annotations.html new file mode 100644 index 00000000..1d11437a --- /dev/null +++ b/documentation/test_python/inspect_annotations/inspect_annotations.html @@ -0,0 +1,105 @@ + + + + + inspect_annotations | My Python Project + + + + + +
+
+
+
+
+

+ inspect_annotations module +

+
+

Contents

+ +
+
+

Classes

+
+
class Foo
+
A class with properties
+
+
+
+

Functions

+
+
+ def annotated_positional_keyword(bar = False, *, + foo: str, + **kwargs) +
+
Function with explicitly delimited keyword args and type annotations
+
+ def annotation(param: typing.List[int], + another: bool, + third: str = 'hello') -> inspect_annotations.Foo +
+
An annotated function
+
+ def args_kwargs(a, b, *args, **kwargs) +
+
Function with args and kwargs
+
+ def no_annotation(a, b, z) +
+
Non-annotated function
+
+ def no_annotation_default_param(param, + another, + third = 'hello') +
+
Non-annotated function
+
+ def partial_annotation(foo, + param: typing.Tuple[int, int], + unannotated, + cls: inspect_annotations.Foo) +
+
Partially annotated function
+
+ def positional_keyword(positional_kw, *, kw_only) +
+
Function with explicitly delimited keyword args
+
+
+
+

Data

+
+
+ ANNOTATED_VAR: typing.Tuple[bool, str] = (False, 'No.') +
+
+
+ UNANNOTATED_VAR = 3.45 +
+
+
+
+
+
+
+
+ + diff --git a/documentation/test_python/inspect_annotations/inspect_annotations.py b/documentation/test_python/inspect_annotations/inspect_annotations.py new file mode 100644 index 00000000..f2e2052b --- /dev/null +++ b/documentation/test_python/inspect_annotations/inspect_annotations.py @@ -0,0 +1,46 @@ +from typing import List, Tuple + +class Foo: + """A class with properties""" + + @property + def a_property(self) -> List[bool]: + """A property with a type annotation""" + pass + +def annotation(param: List[int], another: bool, third: str = "hello") -> Foo: + """An annotated function""" + pass + +def no_annotation(a, b, z): + """Non-annotated function""" + pass + +def no_annotation_default_param(param, another, third = "hello"): + """Non-annotated function""" + pass + +def partial_annotation(foo, param: Tuple[int, int], unannotated, cls: Foo): + """Partially annotated function""" + pass + +# Only possible with native code now, https://www.python.org/dev/peps/pep-0570/ +#def positionals_only(positional_only, /, positional_kw): + #"""Function with explicitly delimited positional args""" + #pass + +def args_kwargs(a, b, *args, **kwargs): + """Function with args and kwargs""" + pass + +def positional_keyword(positional_kw, *, kw_only): + """Function with explicitly delimited keyword args""" + pass + +def annotated_positional_keyword(bar = False, *, foo: str, **kwargs): + """Function with explicitly delimited keyword args and type annotations""" + pass + +UNANNOTATED_VAR = 3.45 + +ANNOTATED_VAR: Tuple[bool, str] = (False, 'No.') diff --git a/documentation/test_python/inspect_annotations/math.html b/documentation/test_python/inspect_annotations/math.html new file mode 100644 index 00000000..90c1e2f1 --- /dev/null +++ b/documentation/test_python/inspect_annotations/math.html @@ -0,0 +1,57 @@ + + + + + math | My Python Project + + + + + +
+
+
+
+
+

+ math module +

+

This module is always available. It provides access to the +mathematical functions defined by the C standard.

+
+

Contents

+ +
+
+

Functions

+
+
+ def pow(x, y, /) +
+
Return x**y (x to the power of y).
+
+ def log(...) +
+
log(x, [base=math.e]) +Return the logarithm of x to the given base.
+
+
+
+
+
+
+ + diff --git a/documentation/test_python/inspect_string/inspect_string.Foo.html b/documentation/test_python/inspect_string/inspect_string.Foo.html new file mode 100644 index 00000000..b7cdce3e --- /dev/null +++ b/documentation/test_python/inspect_string/inspect_string.Foo.html @@ -0,0 +1,107 @@ + + + + + inspect_string.Foo | My Python Project + + + + + +
+
+
+
+
+

+ inspect_string.Foo class +

+

The foo class

+
+

Contents

+ +
+
+

Classes

+
+
class Subclass
+
A subclass of Foo
+
+
+
+

Class methods

+
+
+ def func_on_class(a) +
+
A class method
+
+
+
+

Static methods

+
+
+ def static_func(a) +
+
A static method
+
+
+
+

Methods

+
+
+ def func(self, a, b) +
+
A method
+
+
+
+

Properties

+
+
+ a_property get +
+
A property
+
+ deletable_property get del +
+
Deletable property
+
+ writable_property get set +
+
Writable property
+
+
+
+

Data

+
+
+ A_DATA = 'BOO' +
+
+
+
+
+
+
+
+ + diff --git a/documentation/test_python/inspect_string/inspect_string.Specials.html b/documentation/test_python/inspect_string/inspect_string.Specials.html new file mode 100644 index 00000000..23e86741 --- /dev/null +++ b/documentation/test_python/inspect_string/inspect_string.Specials.html @@ -0,0 +1,59 @@ + + + + + inspect_string.Specials | My Python Project + + + + + +
+
+ + diff --git a/documentation/test_python/inspect_string/inspect_string.another_module.html b/documentation/test_python/inspect_string/inspect_string.another_module.html new file mode 100644 index 00000000..78053490 --- /dev/null +++ b/documentation/test_python/inspect_string/inspect_string.another_module.html @@ -0,0 +1,31 @@ + + + + + inspect_string.another_module | My Python Project + + + + + +
+
+
+
+
+

+ inspect_string.another_module module +

+

Another module

+
+
+
+
+ + diff --git a/documentation/test_python/inspect_string/inspect_string.html b/documentation/test_python/inspect_string/inspect_string.html new file mode 100644 index 00000000..eb283b03 --- /dev/null +++ b/documentation/test_python/inspect_string/inspect_string.html @@ -0,0 +1,85 @@ + + + + + inspect_string | My Python Project + + + + + +
+
+ + diff --git a/documentation/test_python/inspect_string/inspect_string/__init__.py b/documentation/test_python/inspect_string/inspect_string/__init__.py new file mode 100644 index 00000000..987df07a --- /dev/null +++ b/documentation/test_python/inspect_string/inspect_string/__init__.py @@ -0,0 +1,121 @@ +"""A module""" + +# This one is a module that shouldn't be exposed +import enum + +# This one is a package that shouldn't be exposed +import xml + +# These are descendant packages / modules that should be exposed if not +# underscored +from . import subpackage, another_module, _private_module + +# These are variables from an external modules, shouldn't be exposed either +from re import I + +# TODO: there's no way to tell where a variable of a builtin type comes from, +# so this would be exposed. The only solution is requiring a presence of an +# external docstring I'm afraid. +#from math import pi + +class Foo: + """The foo class""" + + A_DATA = 'BOO' + + class Subclass: + """A subclass of Foo""" + pass + + class _PrivateSubclass: + """A private subclass""" + pass + + def func(self, a, b): + """A method""" + pass + + def _private_func(self, a, b): + """A private function""" + pass + + @classmethod + def func_on_class(cls, a): + """A class method""" + pass + + @classmethod + def _private_func_on_class(cls, a): + """A private class method""" + pass + + @staticmethod + def static_func(a): + """A static method""" + pass + + @staticmethod + def _private_static_func(a): + """A private static method""" + pass + + @property + def a_property(self): + """A property""" + pass + + @property + def writable_property(self): + """Writable property""" + pass + + @writable_property.setter + def writable_property(self, a): + pass + + @property + def deletable_property(self): + """Deletable property""" + pass + + @deletable_property.deleter + def deletable_property(self): + pass + + @property + def _private_property(self): + """A private property""" + pass + +class Specials: + """Special class members""" + + def __init__(self): + """The constructor""" + pass + + def __add__(self, other): + """Add a thing""" + pass + + def __and__(self, other): + # Undocumented, but should be present + pass + +class _PrivateClass: + """Private class""" + pass + +def function(): + """A function""" + pass + +def _private_function(): + """A private function""" + pass + +A_CONSTANT = 3.24 + +_PRIVATE_CONSTANT = -3 + +foo = Foo() diff --git a/documentation/test_python/inspect_string/inspect_string/_private_module.py b/documentation/test_python/inspect_string/inspect_string/_private_module.py new file mode 100644 index 00000000..f2ec18f2 --- /dev/null +++ b/documentation/test_python/inspect_string/inspect_string/_private_module.py @@ -0,0 +1 @@ +"""Private module.""" diff --git a/documentation/test_python/inspect_string/inspect_string/another_module.py b/documentation/test_python/inspect_string/inspect_string/another_module.py new file mode 100644 index 00000000..1681ee4c --- /dev/null +++ b/documentation/test_python/inspect_string/inspect_string/another_module.py @@ -0,0 +1,15 @@ +"""Another module""" + +# This one is a module that shouldn't be exposed +import enum + +# This one is a package that shouldn't be exposed +import xml + +# These are variables from an external modules, shouldn't be exposed either +from re import I + +# TODO: there's no way to tell where a variable of a builtin type comes from, +# so this would be exposed. The only solution is requiring a presence of an +# external docstring I'm afraid. +#from math import pi diff --git a/documentation/test_python/inspect_string/inspect_string/subpackage/__init__.py b/documentation/test_python/inspect_string/inspect_string/subpackage/__init__.py new file mode 100644 index 00000000..1fc06e45 --- /dev/null +++ b/documentation/test_python/inspect_string/inspect_string/subpackage/__init__.py @@ -0,0 +1 @@ +"""A subpackage""" diff --git a/documentation/test_python/layout/index.html b/documentation/test_python/layout/index.html new file mode 100644 index 00000000..e17777f3 --- /dev/null +++ b/documentation/test_python/layout/index.html @@ -0,0 +1,106 @@ + + + + + A project | A project is cool + + + + + + + + + + +
+
+ + + +
+ + diff --git a/documentation/test_python/layout/sitemap.xml b/documentation/test_python/layout/sitemap.xml new file mode 100644 index 00000000..e69de29b diff --git a/documentation/test_python/test_inspect.py b/documentation/test_python/test_inspect.py new file mode 100644 index 00000000..09a34a30 --- /dev/null +++ b/documentation/test_python/test_inspect.py @@ -0,0 +1,91 @@ +# +# 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 +import sys +import math + +from . import BaseTestCase + +class String(BaseTestCase): + def __init__(self, *args, **kwargs): + super().__init__(__file__, 'string', *args, **kwargs) + + def test(self): + self.run_python() + self.assertEqual(*self.actual_expected_contents('inspect_string.html')) + self.assertEqual(*self.actual_expected_contents('inspect_string.another_module.html')) + self.assertEqual(*self.actual_expected_contents('inspect_string.Foo.html')) + self.assertEqual(*self.actual_expected_contents('inspect_string.Specials.html')) + +class Object(BaseTestCase): + def __init__(self, *args, **kwargs): + super().__init__(__file__, 'object', *args, **kwargs) + + def test(self): + # Reuse the stuff from inspect_string, but this time reference it via + # an object and not a string + sys.path.append(os.path.join(os.path.dirname(self.path), 'inspect_string')) + import inspect_string + self.run_python({ + 'INPUT_MODULES': [inspect_string] + }) + + # The output should be the same as when inspecting a string + self.assertEqual(*self.actual_expected_contents('inspect_string.html', '../inspect_string/inspect_string.html')) + self.assertEqual(*self.actual_expected_contents('inspect_string.another_module.html', '../inspect_string/inspect_string.another_module.html')) + self.assertEqual(*self.actual_expected_contents('inspect_string.Foo.html', '../inspect_string/inspect_string.Foo.html')) + self.assertEqual(*self.actual_expected_contents('inspect_string.Specials.html', '../inspect_string/inspect_string.Specials.html')) + +class AllProperty(BaseTestCase): + def __init__(self, *args, **kwargs): + super().__init__(__file__, 'all_property', *args, **kwargs) + + def test(self): + self.run_python() + self.assertEqual(*self.actual_expected_contents('inspect_all_property.html')) + +class Annotations(BaseTestCase): + def __init__(self, *args, **kwargs): + super().__init__(__file__, 'annotations', *args, **kwargs) + + def test(self): + self.run_python() + self.assertEqual(*self.actual_expected_contents('inspect_annotations.html')) + self.assertEqual(*self.actual_expected_contents('inspect_annotations.Foo.html')) + + def test_math(self): + # From math export only pow() so we have the verification easier, and + # in addition log() because it doesn't provide any signature metadata + assert not hasattr(math, '__all__') + math.__all__ = ['pow', 'log'] + + self.run_python({ + 'INPUT_MODULES': [math] + }) + + del math.__all__ + assert not hasattr(math, '__all__') + + self.assertEqual(*self.actual_expected_contents('math.html')) diff --git a/documentation/test_python/test_layout.py b/documentation/test_python/test_layout.py new file mode 100644 index 00000000..1d7ca6bb --- /dev/null +++ b/documentation/test_python/test_layout.py @@ -0,0 +1,65 @@ +# +# 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 BaseTestCase + +class Layout(BaseTestCase): + def __init__(self, *args, **kwargs): + super().__init__(__file__, '', *args, **kwargs) + + def test(self): + self.run_python({ + 'PROJECT_TITLE': "A project", + 'PROJECT_SUBTITLE': "is cool", + + 'INPUT_MODULES': [], # Explicitly none + + 'THEME_COLOR': '#00ffff', + 'FAVICON': 'favicon-light.png', + 'PAGE_HEADER': "`A self link <{filename}>`_", + 'FINE_PRINT': "This beautiful thing is done thanks to\n`m.css `_.", + 'LINKS_NAVBAR1': [ + ('Pages', 'pages', [ + ('Getting started', 'getting-started'), + ('Troubleshooting', 'troubleshooting')]), + ('Modules', 'modules', [])], + 'LINKS_NAVBAR2': [ + ('Classes', 'classes', []), + ('GitHub', 'https://github.com/mosra/m.css', [ + ('About', 'about')])], + 'SEARCH_DISABLED': False, + 'SEARCH_EXTERNAL_URL': 'https://google.com/search?q=site:mcss.mosra.cz+{}', + 'SEARCH_HELP': "Some *help*.\nOn multiple lines.", + 'HTML_HEADER': +""" + +""", + 'EXTRA_FILES': ['sitemap.xml'] + }) + self.assertEqual(*self.actual_expected_contents('index.html')) + self.assertTrue(os.path.exists(os.path.join(self.path, 'output/m-dark+documentation.compiled.css'))) + self.assertTrue(os.path.exists(os.path.join(self.path, 'output/favicon-light.png'))) + self.assertTrue(os.path.exists(os.path.join(self.path, 'output/sitemap.xml'))) diff --git a/package/ci/travis.yml b/package/ci/travis.yml index c90b1782..0be5829f 100644 --- a/package/ci/travis.yml +++ b/package/ci/travis.yml @@ -8,7 +8,7 @@ matrix: python: 3.4 env: - WITH_THEME=ON - - WITH_DOXYGEN=OFF + - WITH_DOCUMENTATION=OFF - WITH_NODE=OFF - JOBID=py34 - language: python @@ -19,7 +19,7 @@ matrix: - graphviz env: - WITH_THEME=ON - - WITH_DOXYGEN=OFF + - WITH_DOCUMENTATION=OFF - WITH_NODE=OFF - JOBID=py35 - language: python @@ -30,7 +30,7 @@ matrix: - graphviz env: - WITH_THEME=ON - - WITH_DOXYGEN=ON + - WITH_DOCUMENTATION=ON - WITH_NODE=OFF - TRAVIS_BROKEN_MATPLOTLIB_SEED=YES - JOBID=py36 @@ -38,7 +38,7 @@ matrix: node_js: 8 env: - WITH_THEME=OFF - - WITH_DOXYGEN=OFF + - WITH_DOCUMENTATION=OFF - WITH_NODE=ON - JOBID=js @@ -50,21 +50,22 @@ install: - if [ "$WITH_NODE" == "ON" ]; then npm install istanbul codecov; fi # Needed for doxygen binaries - - if [ "$WITH_DOXYGEN" == "ON" ]; then mkdir -p $HOME/bin && export PATH=$HOME/bin:$PATH; fi + - if [ "$WITH_DOCUMENTATION" == "ON" ]; then mkdir -p $HOME/bin && export PATH=$HOME/bin:$PATH; fi # Download newer Doxygen, as I don't want to care for the old bugs - - if [ "$WITH_DOXYGEN" == "ON" ] && [ ! -f $HOME/bin/doxygen ]; then wget "http://doxygen.nl/files/doxygen-1.8.15.linux.bin.tar.gz" && tar -xzf doxygen-1.8.15.linux.bin.tar.gz && cp doxygen-1.8.15/bin/doxygen $HOME/bin && doxygen -v; fi + - if [ "$WITH_DOCUMENTATION" == "ON" ] && [ ! -f $HOME/bin/doxygen ]; then wget "http://doxygen.nl/files/doxygen-1.8.15.linux.bin.tar.gz" && tar -xzf doxygen-1.8.15.linux.bin.tar.gz && cp doxygen-1.8.15/bin/doxygen $HOME/bin && doxygen -v; fi script: # Test the theme. No code coverage there. - if [ "$WITH_THEME" == "ON" ]; then cd $TRAVIS_BUILD_DIR/pelican-theme && python -m unittest; fi - # Test plugins. Math plugin is not tested as dvisvgm on 14.04 is unusable. + # Test plugins. Math plugin is not tested as dvisvgm is unusable even on + # 16.04. - if [ "$WITH_THEME" == "ON" ]; then cd $TRAVIS_BUILD_DIR/plugins && coverage run -m unittest && cp .coverage ../.coverage.plugins; fi - # Test doxygen theme. Math rendering is not tested as dvisvgm on 14.04 is - # unusable. - - if [ "$WITH_DOXYGEN" == "ON" ]; then cd $TRAVIS_BUILD_DIR/documentation && coverage run -m unittest && cp .coverage ../.coverage.doxygen; fi + # Test documentation themes. Math rendering is not tested as dvisvgm is + # unusable even on 16.04. + - if [ "$WITH_DOCUMENTATION" == "ON" ]; then cd $TRAVIS_BUILD_DIR/documentation && coverage run -m unittest && cp .coverage ../.coverage.doxygen; fi # Test client doxygen JS - if [ "$WITH_NODE" == "ON" ]; then cd $TRAVIS_BUILD_DIR/documentation && node ../node_modules/istanbul/lib/cli.js cover test_doxygen/test-search.js; fi @@ -89,5 +90,5 @@ notifications: on_start: never after_success: - - if [ "$WITH_THEME" == "ON" ] || [ "$WITH_DOXYGEN" == "ON" ]; then cd $TRAVIS_BUILD_DIR && coverage combine && codecov; fi + - if [ "$WITH_THEME" == "ON" ] || [ "$WITH_DOCUMENTATION" == "ON" ]; then cd $TRAVIS_BUILD_DIR && coverage combine && codecov; fi - if [ "$WITH_NODE" == "ON" ]; then cd $TRAVIS_BUILD_DIR && codecov; fi diff --git a/site/pelicanconf.py b/site/pelicanconf.py index 4eed9701..706ad5b6 100644 --- a/site/pelicanconf.py +++ b/site/pelicanconf.py @@ -83,7 +83,8 @@ M_LINKS_NAVBAR1 = [('Why?', 'why/', 'why', []), ('Pelican', 'themes/pelican/', 'themes/pelican')])] M_LINKS_NAVBAR2 = [('Doc generators', 'documentation/', 'documentation', [ - ('Doxygen C++ theme', 'documentation/doxygen/', 'documentation/doxygen')]), + ('Doxygen C++ theme', 'documentation/doxygen/', 'documentation/doxygen'), + ('Python doc theme', 'documentation/python/', 'documentation/python')]), ('Plugins', 'plugins/', 'plugins', [ ('HTML sanity', 'plugins/htmlsanity/', 'plugins/htmlsanity'), ('Components', 'plugins/components/', 'plugins/components'), @@ -114,7 +115,8 @@ M_LINKS_FOOTER3 = [('Themes', 'themes/'), ('Pelican', 'themes/pelican/'), ('', ''), ('Doc generators', 'documentation/'), - ('Doxygen C++ theme', 'documentation/doxygen/')] + ('Doxygen C++ theme', 'documentation/doxygen/'), + ('Python documentation', 'documentation/python/')] M_LINKS_FOOTER4 = [('Plugins', 'plugins/'), ('HTML sanity', 'plugins/htmlsanity/'),