<td class="m-dim"></td>
</tr>
<tr>
- <th class="m-text-right">Doxygen theme</th>
+ <th class="m-text-right">Documentation themes</th>
<td class="m-dim"></td>
<td class="m-dim"></td>
</tr>
<tr>
- <th class="m-text-right">Doxygen theme<br/>client search</th>
+ <th class="m-text-right">Documentation themes<br/>client search</th>
<td class="m-dim"></td>
<td class="m-dim"></td>
<td id="mcss-js"><a> <br/><span class="m-text m-small"> </span></a></td>
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.
: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
================
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
--- /dev/null
+..
+ This file is part of m.css.
+
+ Copyright © 2017, 2018, 2019 Vladimír Vondruš <mosra@centrum.cz>
+
+ 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 <reStructuredText>`
+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 <https://magnum.graphics>`_ 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 <reStructuredText>` 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 <mosra/m.css$master/documentation>` 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 <http://jinja.pocoo.org/>`_
+for templating and docutils for :abbr:`reST <reStructuredText>` 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 <reStructuredText>`
+ 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:`<meta name="theme-color" />`,
+ corresponding to the CSS style. If empty,
+ no :html:`<meta>` tag is rendered. See
+ `Theme selection`_ for more information.
+:py:`FAVICON: str` Favicon URL, used to populate
+ :html:`<link rel="icon" />`. If empty, no
+ :html:`<link>` 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:`<head>` element. Useful for linking
+ arbitrary JavaScript code or, for example,
+ adding :html:`<link>` 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 <reStructuredText>` 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 <reStructuredText>` 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 <reStructuredText>` 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 <http://docutils.sourceforge.net/docs/user/config.html>`_.
+=================================== ===========================================
+
+`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 <https://bugs.chromium.org/p/chromium/issues/detail?id=40787&q=ajax%20local&colspec=ID%20Stars%20Pri%20Area%20Feature%20Type%20Status%20Summary%20Modified%20Owner%20Mstone%20OS>`_,
+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 <http://www.opensearch.org/>`_
+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 <https://www.python.org/dev/peps/pep-0570/>`_ 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:`<link rel="stylesheet" />` 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 %}
+ <link rel="stylesheet" href="{{ css|basename_or_url }}" />
+ {% 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 <https://docs.python.org/3/library/inspect.html#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.
node_modules/
package-lock.json
test_doxygen/package-lock.json
+test_python/*/output/
--- /dev/null
+#!/usr/bin/env python3
+
+#
+# This file is part of m.css.
+#
+# Copyright © 2017, 2018, 2019 Vladimír Vondruš <mosra@centrum.cz>
+#
+# 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
+
+ <p class="m-noindent">Search for modules, classes, functions and other
+ symbols. You can omit any prefix from the symbol path; adding a <code>.</code>
+ suffix lists all members of given symbol.</p>
+ <p class="m-noindent">Use <span class="m-label m-dim">↓</span>
+ / <span class="m-label m-dim">↑</span> to navigate through the list,
+ <span class="m-label m-dim">Enter</span> to go.
+ <span class="m-label m-dim">Tab</span> autocompletes common prefix, you can
+ copy a link to the result using <span class="m-label m-dim">⌘</span>
+ <span class="m-label m-dim">L</span> while <span class="m-label m-dim">⌘</span>
+ <span class="m-label m-dim">M</span> produces a Markdown link.</p>
+""",
+ '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 <class 'foo.bar'> 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 <foo.bar at 0xbadbeef> 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<value."),
+ ('__ne__', "Return self!=value."),
+ ('__new__',
+ "Create and return a new object. See help(type) for accurate signature."),
+ ('__reduce__', "Helper for pickle."),
+ ('__reduce_ex__', "Helper for pickle."),
+ ('__repr__', "Return repr(self)."),
+ ('__setattr__', "Implement setattr(self, name, value)."),
+ ('__sizeof__', "Size of object in memory, in bytes."),
+ ('__str__', "Return str(self)."),
+ ('__subclasshook__',
+ "Abstract classes can override this to customize issubclass().\n\n"
+ "This is invoked early on by abc.ABCMeta.__subclasscheck__().\n"
+ "It should return True, False or NotImplemented. If it returns\n"
+ "NotImplemented, the normal algorithm is used. Otherwise, it\n"
+ "overrides the normal algorithm (and the outcome is cached).\n")
+])
+
+_filtered_builtin_properties = set([
+ ('__weakref__', "list of weak references to the object (if defined)")
+])
+
+def render_class(config, path, class_, 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(class_.__doc__)
+ page.url = breadcrumb[-1][1]
+ page.breadcrumb = breadcrumb
+ page.classes = []
+ page.classmethods = []
+ page.staticmethods = []
+ page.dunder_methods = []
+ page.methods = []
+ page.properties = []
+ page.data = []
+
+ # Get inner classes
+ for name, object in inspect.getmembers(class_, inspect.isclass):
+ if name in ['__base__', '__class__']: continue # TODO
+ if name.startswith('_'): 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 methods
+ for name, object in inspect.getmembers(class_, inspect.isroutine):
+ # Filter out underscored methods (but not dunder methods)
+ if is_internal_function_name(name): continue
+
+ # Filter out dunder methods that don't have their own docs
+ if name.startswith('__') and (name, object.__doc__) in _filtered_builtin_functions: continue
+
+ subpath = path + [name]
+ if not object.__doc__: logging.warning("%s() is undocumented", '.'.join(subpath))
+
+ function = extract_function_doc(subpath, object)
+ function.is_classmethod = inspect.ismethod(object)
+ function.is_staticmethod = name in class_.__dict__ and isinstance(class_.__dict__[name], staticmethod)
+
+ if name.startswith('__'):
+ page.dunder_methods += [function]
+ elif function.is_classmethod:
+ page.classmethods += [function]
+ elif function.is_staticmethod:
+ page.staticmethods += [function]
+ else:
+ page.methods += [function]
+
+ # Get properties
+ for name, object in inspect.getmembers(class_, inspect.isdatadescriptor):
+ if (name, object.__doc__) in _filtered_builtin_properties:
+ continue
+ if name.startswith('_'): continue # TODO: are there any dunder props?
+
+ subpath = path + [name]
+ if not object.__doc__: logging.warning("%s is undocumented", '.'.join(subpath))
+
+ page.properties += [extract_property_doc(subpath, object)]
+
+ # Get data
+ # TODO: unify this query
+ for name, object in inspect.getmembers(class_, 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) and not inspect.isdatadescriptor(o)):
+ if name.startswith('_'): continue
+
+ subpath = path + [name]
+ page.data += [extract_data_doc(class_, subpath, object)]
+
+ render(config, 'class.html', page, env)
+
+def run(basedir, config, templates):
+ # Prepare Jinja environment
+ env = jinja2.Environment(
+ loader=jinja2.FileSystemLoader(templates), trim_blocks=True,
+ lstrip_blocks=True, enable_async=True)
+ # Filter to return file basename or the full URL, if absolute
+ def basename_or_url(path):
+ if urllib.parse.urlparse(path).netloc: return path
+ return os.path.basename(path)
+ # Filter to return URL for given symbol or the full URL, if absolute
+ def path_to_url(path):
+ if urllib.parse.urlparse(path).netloc: return path
+ return path + '.html'
+ env.filters['basename_or_url'] = basename_or_url
+ env.filters['path_to_url'] = path_to_url
+ env.filters['urljoin'] = urljoin
+ env.filters['render_rst'] = m.htmlsanity.render_rst
+
+ # Make the output dir absolute
+ config['OUTPUT'] = os.path.join(basedir, config['OUTPUT'])
+ if not os.path.exists(config['OUTPUT']): os.makedirs(config['OUTPUT'])
+
+ # Guess MIME type of the favicon
+ if config['FAVICON']:
+ config['FAVICON'] = (config['FAVICON'], mimetypes.guess_type(config['FAVICON'])[0])
+
+ for module in config['INPUT_MODULES']:
+ if isinstance(module, str):
+ module_name = module
+ module = importlib.import_module(module)
+ else:
+ module_name = module.__name__
+
+ render_module(config, [module_name], module, env)
+
+ # Create index.html
+ # TODO: use actual reST source and have this just as a fallback
+ page = Empty()
+ page.breadcrumb = [(config['PROJECT_TITLE'], 'index.html')]
+ page.url = page.breadcrumb[-1][1]
+ render(config, 'page.html', page, env)
+
+ # Copy referenced files
+ for i in config['STYLESHEETS'] + config['EXTRA_FILES'] + ([config['FAVICON'][0]] if config['FAVICON'] else []) + ([] if config['SEARCH_DISABLED'] else ['search.js']):
+ # Skip absolute URLs
+ if urllib.parse.urlparse(i).netloc: continue
+
+ # If file is found relative to the conf file, use that
+ if os.path.exists(os.path.join(basedir, i)):
+ i = os.path.join(basedir, i)
+
+ # Otherwise use path relative to script directory
+ else:
+ i = os.path.join(os.path.dirname(os.path.realpath(__file__)), i)
+
+ logging.debug("copying %s to output", i)
+ shutil.copy(i, os.path.join(config['OUTPUT'], os.path.basename(i)))
+
+if __name__ == '__main__': # pragma: no cover
+ parser = argparse.ArgumentParser()
+ parser.add_argument('conf', help="configuration file")
+ parser.add_argument('--templates', help="template directory", default=default_templates)
+ parser.add_argument('--debug', help="verbose debug output", action='store_true')
+ args = parser.parse_args()
+
+ # Load configuration from a file, update the defaults with it
+ config = copy.deepcopy(default_config)
+ name, _ = os.path.splitext(os.path.basename(args.conf))
+ module = SourceFileLoader(name, args.conf).load_module()
+ if module is not None:
+ config.update((k, v) for k, v in inspect.getmembers(module) if k.isupper())
+
+ if args.debug:
+ logging.basicConfig(level=logging.DEBUG)
+ else:
+ logging.basicConfig(level=logging.INFO)
+
+ run(os.path.dirname(os.path.abspath(args.conf)), config, os.path.abspath(args.templates))
--- /dev/null
+<!DOCTYPE html>
+<html lang="en">
+<head>
+ <meta charset="UTF-8" />
+ <title>{% block title %}{{ PROJECT_TITLE }}{% if PROJECT_SUBTITLE %} {{ PROJECT_SUBTITLE }}{% endif %}{% endblock %}</title>
+ {% for css in STYLESHEETS %}
+ <link rel="stylesheet" href="{{ css|basename_or_url|e }}" />
+ {% endfor %}
+ {% if FAVICON %}
+ <link rel="icon" href="{{ FAVICON[0]|basename_or_url|e }}" type="{{ FAVICON[1] }}" />
+ {% endif %}
+ {% if not SEARCH_DISABLED and SEARCH_BASE_URL %}
+ <link rel="search" type="application/opensearchdescription+xml" href="opensearch.xml" title="Search {{ PROJECT_TITLE }} documentation" />
+ {% endif %}
+ {% block header_links %}
+ {% endblock %}
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+ {% if THEME_COLOR %}
+ <meta name="theme-color" content="{{ THEME_COLOR }}" />
+ {% endif %}
+ {% if HTML_HEADER %}
+ {{ HTML_HEADER|indent(2) }}
+ {% endif %}
+</head>
+<body>
+<header><nav id="navigation">
+ <div class="m-container">
+ <div class="m-row">
+ {% if MAIN_PROJECT_URL and PROJECT_TITLE %}
+ <span id="m-navbar-brand" class="m-col-t-8 m-col-m-none m-left-m">
+ <a href="{{ MAIN_PROJECT_URL }}">{{ PROJECT_TITLE }}</a> <span class="m-breadcrumb">|</span> <a href="index.html" class="m-thin">{{ PROJECT_SUBTITLE }}</a>
+ </span>
+ {% else %}
+ <a href="index.html" id="m-navbar-brand" class="m-col-t-8 m-col-m-none m-left-m">{{ PROJECT_TITLE }}{% if PROJECT_SUBTITLE %} <span class="m-thin">{{ PROJECT_SUBTITLE }}</span>{% endif %}</a>
+ {% endif %}
+ {% if LINKS_NAVBAR1 or LINKS_NAVBAR2 or not SEARCH_DISABLED %}
+ <div class="m-col-t-4 m-hide-m m-text-right m-nopadr">
+ {% if not SEARCH_DISABLED %}
+ <a href="#search" class="m-doc-search-icon" title="Search" onclick="return showSearch()"><svg style="height: 0.9rem;" viewBox="0 0 16 16">
+ <path d="m6 0c-3.3144 0-6 2.6856-6 6 0 3.3144 2.6856 6 6 6 1.4858 0 2.8463-0.54083 3.8945-1.4355-0.0164 0.33797 0.14734 0.75854 0.5 1.1504l3.2227 3.7891c0.55185 0.6139 1.4517 0.66544 2.002 0.11524 0.55022-0.55022 0.49866-1.4501-0.11524-2.002l-3.7891-3.2246c-0.39184-0.35266-0.81242-0.51469-1.1504-0.5 0.89472-1.0482 1.4355-2.4088 1.4355-3.8945 0-3.3128-2.6856-5.998-6-5.998zm0 1.5625a4.4375 4.4375 0 0 1 4.4375 4.4375 4.4375 4.4375 0 0 1-4.4375 4.4375 4.4375 4.4375 0 0 1-4.4375-4.4375 4.4375 4.4375 0 0 1 4.4375-4.4375z"/>
+ </svg></a>
+ {% endif %}
+ <a id="m-navbar-show" href="#navigation" title="Show navigation"></a>
+ <a id="m-navbar-hide" href="#" title="Hide navigation"></a>
+ </div>
+ <div id="m-navbar-collapse" class="m-col-t-12 m-show-m m-col-m-none m-right-m">
+ <div class="m-row">
+ <ol class="{% if M_LINKS_NAVBAR2 %}m-col-t-6{% else %}m-col-t-12{% endif %} m-col-m-none">
+ {% for title, path, sub in LINKS_NAVBAR1 %}
+ {% if not sub %}
+ <li><a href="{{ path|path_to_url }}"{% if (page and page.path == path) or navbar_current == path %} id="m-navbar-current"{% endif %}>{{ title }}</a></li>
+ {% else %}
+ <li>
+ <a href="{{ path|path_to_url }}"{% if (page and page.path == path) or navbar_current == path %} id="m-navbar-current"{% endif %}>{{ title }}</a>
+ <ol>
+ {% for title, path in sub %}
+ <li><a href="{{ path|path_to_url }}"{% if (page and page.path == path) or navbar_current == path %} id="m-navbar-current"{% endif %}>{{ title }}</a></li>
+ {% endfor %}
+ </ol>
+ </li>
+ {% endif %}
+ {% endfor %}
+ </ol>
+ {% if LINKS_NAVBAR2 or not SEARCH_DISABLED %}
+ {% set start = LINKS_NAVBAR1|length + 1 %}
+ <ol class="m-col-t-6 m-col-m-none" start="{{ start }}">
+ {% for title, path, sub in LINKS_NAVBAR2 %}
+ {% if not sub %}
+ <li><a href="{{ path|path_to_url }}"{% if (page and page.path == path) or navbar_current == path %} id="m-navbar-current"{% endif %}>{{ title }}</a></li>
+ {% else %}
+ <li>
+ <a href="{{ path|path_to_url }}"{% if (page and page.path == path) or navbar_current == path %} id="m-navbar-current"{% endif %}>{{ title }}</a>
+ <ol>
+ {% for title, path in sub %}
+ <li><a href="{{ path|path_to_url }}"{% if (page and page.path == path) or navbar_current == path %} id="m-navbar-current"{% endif %}>{{ title }}</a></li>
+ {% endfor %}
+ </ol>
+ </li>
+ {% endif %}
+ {% endfor %}
+ {% if not SEARCH_DISABLED %}
+ <li class="m-show-m"><a href="#search" class="m-doc-search-icon" title="Search" onclick="return showSearch()"><svg style="height: 0.9rem;" viewBox="0 0 16 16">
+ <path d="m6 0c-3.3144 0-6 2.6856-6 6 0 3.3144 2.6856 6 6 6 1.4858 0 2.8463-0.54083 3.8945-1.4355-0.0164 0.33797 0.14734 0.75854 0.5 1.1504l3.2227 3.7891c0.55185 0.6139 1.4517 0.66544 2.002 0.11524 0.55022-0.55022 0.49866-1.4501-0.11524-2.002l-3.7891-3.2246c-0.39184-0.35266-0.81242-0.51469-1.1504-0.5 0.89472-1.0482 1.4355-2.4088 1.4355-3.8945 0-3.3128-2.6856-5.998-6-5.998zm0 1.5625a4.4375 4.4375 0 0 1 4.4375 4.4375 4.4375 4.4375 0 0 1-4.4375 4.4375 4.4375 4.4375 0 0 1-4.4375-4.4375 4.4375 4.4375 0 0 1 4.4375-4.4375z"/>
+ </svg></a></li>
+ {% endif %}
+ </ol>
+ {% endif %}
+ </div>
+ </div>
+ {% endif %}
+ </div>
+ </div>
+</nav></header>
+<main><article>
+ <div class="m-container m-container-inflatable">
+ <div class="m-row">
+ <div class="m-col-l-10 m-push-l-1">
+ {% if PAGE_HEADER %}
+ {{ PAGE_HEADER|render_rst|replace('{filename}', FILENAME) }}
+ {% endif %}
+{% block main %}
+{% endblock %}
+ </div>
+ </div>
+ </div>
+</article></main>
+{% if not SEARCH_DISABLED %}
+<div class="m-doc-search" id="search">
+ <a href="#!" onclick="return hideSearch()"></a>
+ <div class="m-container">
+ <div class="m-row">
+ <div class="m-col-m-8 m-push-m-2">
+ <div class="m-doc-search-header m-text m-small">
+ <div><span class="m-label m-default">Tab</span> / <span class="m-label m-default">T</span> to search, <span class="m-label m-default">Esc</span> to close</div>
+ <div id="search-symbolcount">…</div>
+ </div>
+ <div class="m-doc-search-content">
+ <form{% if SEARCH_BASE_URL %} action="{{ SEARCH_BASE_URL }}#search"{% endif %}>
+ <input type="search" name="q" id="search-input" placeholder="Loading …" disabled="disabled" autofocus="autofocus" autocomplete="off" spellcheck="false" />
+ </form>
+ <noscript class="m-text m-danger m-text-center">Unlike everything else in the docs, the search functionality <em>requires</em> JavaScript.{% if SEARCH_EXTERNAL_URL %} Enable it or <a href="{{ SEARCH_EXTERNAL_URL|replace('{query}', '') }}">use an external search engine</a>.{% endif %}</noscript>
+ <div id="search-help" class="m-text m-dim m-text-center">
+ {{ SEARCH_HELP|render_rst|indent(12) }}
+ </div>
+ <div id="search-notfound" class="m-text m-warning m-text-center">Sorry, nothing was found.{% if SEARCH_EXTERNAL_URL %}<br />Maybe try a full-text <a href="#" id="search-external" data-search-engine="{{ SEARCH_EXTERNAL_URL }}">search with external engine</a>?{% endif %}</div>
+ <ul id="search-results"></ul>
+ </div>
+ </div>
+ </div>
+ </div>
+</div>
+<script src="search.js"></script>
+{% if SEARCH_DOWNLOAD_BINARY %}
+<script>
+ Search.download(window.location.pathname.substr(0, window.location.pathname.lastIndexOf('/') + 1) + "searchdata.bin");
+</script>
+{% else %}
+<script src="searchdata.js" async="async"></script>
+{% endif %}
+{% endif %}
+{% if FINE_PRINT %}
+<footer><nav>
+ <div class="m-container">
+ <div class="m-row">
+ <div class="m-col-l-10 m-push-l-1">
+ {% if FINE_PRINT == '[default]' %}
+ <p>{{ PROJECT_TITLE }}{% if PROJECT_SUBTITLE %} {{ PROJECT_SUBTITLE }}{% endif %}. Created by <a href="https://mcss.mosra.cz/documentation/python/">m.css Python doc generator</a>.</p>
+ {% else %}
+ {{ FINE_PRINT|render_rst|indent(8) }}
+ {% endif %}
+ </div>
+ </div>
+ </div>
+</nav></footer>
+{% endif %}
+</body>
+</html>
--- /dev/null
+{% 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 %}
+ <h1>
+ {%+ for name, target in page.breadcrumb[:-1] %}<span class="m-breadcrumb"><a href="{{ target }}">{{ name }}</a>.<wbr/></span>{% endfor %}{{ page.breadcrumb[-1][0] }} <span class="m-thin">class</span>
+ </h1>
+ {% if page.brief %}
+ <p>{{ page.brief }}</p>
+ {% endif %}
+ {% if page.classes or page.classmethods or page.staticmethods or page.methods or page.dunder_methods or page.properties or page.data %}
+ <div class="m-block m-default">
+ <h3>Contents</h3>
+ <ul>
+ <li>
+ Reference
+ <ul>
+ {% if page.classes %}
+ <li><a href="#classes">Classes</a></li>
+ {% endif %}
+ {% if page.classmethods %}
+ <li><a href="#classmethods">Class methods</a></li>
+ {% endif %}
+ {% if page.staticmethods %}
+ <li><a href="#staticmethods">Static methods</a></li>
+ {% endif %}
+ {% if page.methods %}
+ <li><a href="#methods">Methods</a></li>
+ {% endif %}
+ {% if page.dunder_methods %}
+ <li><a href="#dunder-methods">Special methods</a></li>
+ {% endif %}
+ {% if page.properties %}
+ <li><a href="#properties">Properties</a></li>
+ {% endif %}
+ {% if page.data %}
+ <li><a href="#data">Data</a></li>
+ {% endif %}
+ </ul>
+ </li>
+ </ul>
+ </div>
+ {% endif %}
+ {% if page.classes %}
+ <section id="classes">
+ <h2><a href="#classes">Classes</a></h2>
+ <dl class="m-doc">
+ {% for class in page.classes %}
+{{ entry_class(class) }}
+ {% endfor %}
+ </dl>
+ </section>
+ {% endif %}
+ {% if page.classmethods %}
+ <section id="classmethods">
+ <h2><a href="#classmethods">Class methods</a></h2>
+ <dl class="m-doc">
+ {% for function in page.classmethods %}
+{{ entry_function(function) }}
+ {% endfor %}
+ </dl>
+ </section>
+ {% endif %}
+ {% if page.staticmethods %}
+ <section id="staticmethods">
+ <h2><a href="#staticmethods">Static methods</a></h2>
+ <dl class="m-doc">
+ {% for function in page.staticmethods %}
+{{ entry_function(function) }}
+ {% endfor %}
+ </dl>
+ </section>
+ {% endif %}
+ {% if page.methods %}
+ <section id="methods">
+ <h2><a href="#methods">Methods</a></h2>
+ <dl class="m-doc">
+ {% for function in page.methods %}
+{{ entry_function(function) }}
+ {% endfor %}
+ </dl>
+ </section>
+ {% endif %}
+ {% if page.dunder_methods %}
+ <section id="dunder-methods">
+ <h2><a href="#dunder-methods">Special methods</a></h2>
+ <dl class="m-doc">
+ {% for function in page.dunder_methods %}
+{{ entry_function(function) }}
+ {% endfor %}
+ </dl>
+ </section>
+ {% endif %}
+ {% if page.properties %}
+ <section id="properties">
+ <h2><a href="#properties">Properties</a></h2>
+ <dl class="m-doc">
+ {% for property in page.properties %}
+{{ entry_property(property) }}
+ {% endfor %}
+ </dl>
+ </section>
+ {% endif %}
+ {% if page.data %}
+ <section id="data">
+ <h2><a href="#data">Data</a></h2>
+ <dl class="m-doc">
+ {% for data in page.data %}
+{{ entry_data(data) }}
+ {% endfor %}
+ </dl>
+ </section>
+ {% endif %}
+{% endblock %}
--- /dev/null
+ <dt>class <a href="{{ class.url }}" class="m-doc">{{ class.name }}</a>{% if class.is_deprecated %} <span class="m-label m-danger">deprecated</span>{% endif %}</dt>
+ <dd>{{ class.brief }}</dd>
--- /dev/null
+ <dt>
+ <a href="" class="m-doc{% if not data.has_details %}-self{% endif %}">{{ data.name }}</a>{% if data.type %}: {{ data.type }}{% endif %}{% if data.value %} = {{ data.value }}{% endif %}
+ {# This has to be here to avoid the newline being eaten #}
+
+ </dt>
+ <dd>{{ data.brief }}</dd>
--- /dev/null
+ <dt>
+ {% set j = joiner('\n ' if function.has_complex_params else ' ') %}
+ <span class="m-doc-wrap-bumper">def <a href="" class="m-doc{% if not function.has_details %}-self{% endif %}">{{ function.name }}</a>(</span><span class="m-doc-wrap">{% for param in function.params %}{% if loop.index0 %}{% if function.params[loop.index0 - 1].kind == 'POSITIONAL_OR_KEYWORD' and param.kind == 'KEYWORD_ONLY' %},<span class="m-text m-dim"> *,</span>{% 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') %}<span class="m-text m-dim">, /</span>{% endif %}{% endfor %}){% if function.type %} -> {{ function.type }}{% endif %}</span>
+ </dt>
+ <dd>{{ function.brief }}</dd>
--- /dev/null
+ <dt>module <a href="{{ module.url }}" class="m-doc">{{ module.name }}</a>{% if module.is_deprecated %} <span class="m-label m-danger">deprecated</span>{% endif %}</dt>
+ <dd>{{ module.brief }}</dd>
--- /dev/null
+ <dt>
+ <a href="" class="m-doc{% if not property.has_details %}-self{% endif %}">{{ property.name }}</a>{% if property.type %}: {{ property.type }}{% endif %} <span class="m-label m-flat {% if property.is_settable %}m-success{% else %}m-warning{% endif %}">get{% if property.is_settable %} set{% endif %}{% if property.is_deletable %} del{% endif %}</span>
+ </dt>
+ <dd>{{ property.brief }}</dd>
--- /dev/null
+{% 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 %}
+ <h1>
+ {%+ for name, target in page.breadcrumb[:-1] %}<span class="m-breadcrumb"><a href="{{ target }}">{{ name }}</a>.<wbr/></span>{% endfor %}{{ page.breadcrumb[-1][0] }} <span class="m-thin">module</span>
+ </h1>
+ {% if page.brief %}
+ <p>{{ page.brief }}</p>
+ {% endif %}
+ {% if page.modules or page.classes or page.functions or page.data %}
+ <div class="m-block m-default">
+ <h3>Contents</h3>
+ <ul>
+ <li>
+ Reference
+ <ul>
+ {% if page.modules %}
+ <li><a href="#packages">Modules</a></li>
+ {% endif %}
+ {% if page.classes %}
+ <li><a href="#classes">Classes</a></li>
+ {% endif %}
+ {% if page.functions %}
+ <li><a href="#functions">Functions</a></li>
+ {% endif %}
+ {% if page.data %}
+ <li><a href="#data">Data</a></li>
+ {% endif %}
+ </ul>
+ </li>
+ </ul>
+ </div>
+ {% endif %}
+ {% if page.modules %}
+ <section id="namespaces">
+ <h2><a href="#namespaces">Modules</a></h2>
+ <dl class="m-doc">
+ {% for module in page.modules %}
+{{ entry_module(module) }}
+ {% endfor %}
+ </dl>
+ </section>
+ {% endif %}
+ {% if page.classes %}
+ <section id="classes">
+ <h2><a href="#classes">Classes</a></h2>
+ <dl class="m-doc">
+ {% for class in page.classes %}
+{{ entry_class(class) }}
+ {% endfor %}
+ </dl>
+ </section>
+ {% endif %}
+ {% if page.functions %}
+ <section id="functions">
+ <h2><a href="#functions">Functions</a></h2>
+ <dl class="m-doc">
+ {% for function in page.functions %}
+{{ entry_function(function) }}
+ {% endfor %}
+ </dl>
+ </section>
+ {% endif %}
+ {% if page.data %}
+ <section id="data">
+ <h2><a href="#data">Data</a></h2>
+ <dl class="m-doc">
+ {% for data in page.data %}
+{{ entry_data(data) }}
+ {% endfor %}
+ </dl>
+ </section>
+ {% endif %}
+{% endblock %}
--- /dev/null
+{% extends 'base.html' %}
+
+{% block title %}{% set j = joiner('.') %}{% for name, _ in page.breadcrumb %}{{ j() }}{{ name }}{% endfor %} | {{ super() }}{% endblock %}
+
+{% block main %}
+ <h1>
+ {%+ for name, target in page.breadcrumb[:-1] %}<span class="m-breadcrumb"><a href="{{ target }}">{{ name }}</a>.<wbr/></span>{% endfor %}{{ page.breadcrumb[-1][0] }}
+ </h1>
+{% endblock %}
--- /dev/null
+#
+# This file is part of m.css.
+#
+# Copyright © 2017, 2018, 2019 Vladimír Vondruš <mosra@centrum.cz>
+#
+# 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
--- /dev/null
+<!DOCTYPE html>
+<html lang="en">
+<head>
+ <meta charset="UTF-8" />
+ <title>inspect_all_property | My Python Project</title>
+ <link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Source+Sans+Pro:400,400i,600,600i%7CSource+Code+Pro:400,400i,600" />
+ <link rel="stylesheet" href="m-dark+documentation.compiled.css" />
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+</head>
+<body>
+<header><nav id="navigation">
+ <div class="m-container">
+ <div class="m-row">
+ <a href="index.html" id="m-navbar-brand" class="m-col-t-8 m-col-m-none m-left-m">My Python Project</a>
+ </div>
+ </div>
+</nav></header>
+<main><article>
+ <div class="m-container m-container-inflatable">
+ <div class="m-row">
+ <div class="m-col-l-10 m-push-l-1">
+ <h1>
+ inspect_all_property <span class="m-thin">module</span>
+ </h1>
+ <div class="m-block m-default">
+ <h3>Contents</h3>
+ <ul>
+ <li>
+ Reference
+ <ul>
+ <li><a href="#packages">Modules</a></li>
+ <li><a href="#classes">Classes</a></li>
+ <li><a href="#functions">Functions</a></li>
+ <li><a href="#data">Data</a></li>
+ </ul>
+ </li>
+ </ul>
+ </div>
+ <section id="namespaces">
+ <h2><a href="#namespaces">Modules</a></h2>
+ <dl class="m-doc">
+ <dt>module <a href="inspect_all_property._private_but_exposed.html" class="m-doc">_private_but_exposed</a></dt>
+ <dd></dd>
+ </dl>
+ </section>
+ <section id="classes">
+ <h2><a href="#classes">Classes</a></h2>
+ <dl class="m-doc">
+ <dt>class <a href="inspect_all_property._PrivateButExposedClass.html" class="m-doc">_PrivateButExposedClass</a></dt>
+ <dd></dd>
+ </dl>
+ </section>
+ <section id="functions">
+ <h2><a href="#functions">Functions</a></h2>
+ <dl class="m-doc">
+ <dt>
+ <span class="m-doc-wrap-bumper">def <a href="" class="m-doc-self">_private_but_exposed_func</a>(</span><span class="m-doc-wrap">)</span>
+ </dt>
+ <dd></dd>
+ </dl>
+ </section>
+ <section id="data">
+ <h2><a href="#data">Data</a></h2>
+ <dl class="m-doc">
+ <dt>
+ <a href="" class="m-doc-self">_private_data</a> = 'Hey!'
+ </dt>
+ <dd></dd>
+ </dl>
+ </section>
+ </div>
+ </div>
+ </div>
+</article></main>
+</body>
+</html>
--- /dev/null
+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']
--- /dev/null
+<!DOCTYPE html>
+<html lang="en">
+<head>
+ <meta charset="UTF-8" />
+ <title>inspect_annotations.Foo | My Python Project</title>
+ <link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Source+Sans+Pro:400,400i,600,600i%7CSource+Code+Pro:400,400i,600" />
+ <link rel="stylesheet" href="m-dark+documentation.compiled.css" />
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+</head>
+<body>
+<header><nav id="navigation">
+ <div class="m-container">
+ <div class="m-row">
+ <a href="index.html" id="m-navbar-brand" class="m-col-t-8 m-col-m-none m-left-m">My Python Project</a>
+ </div>
+ </div>
+</nav></header>
+<main><article>
+ <div class="m-container m-container-inflatable">
+ <div class="m-row">
+ <div class="m-col-l-10 m-push-l-1">
+ <h1>
+ <span class="m-breadcrumb"><a href="inspect_annotations.html">inspect_annotations</a>.<wbr/></span>Foo <span class="m-thin">class</span>
+ </h1>
+ <p>A class with properties</p>
+ <div class="m-block m-default">
+ <h3>Contents</h3>
+ <ul>
+ <li>
+ Reference
+ <ul>
+ <li><a href="#properties">Properties</a></li>
+ </ul>
+ </li>
+ </ul>
+ </div>
+ <section id="properties">
+ <h2><a href="#properties">Properties</a></h2>
+ <dl class="m-doc">
+ <dt>
+ <a href="" class="m-doc-self">a_property</a>: typing.List[bool] <span class="m-label m-flat m-warning">get</span>
+ </dt>
+ <dd>A property with a type annotation</dd>
+ </dl>
+ </section>
+ </div>
+ </div>
+ </div>
+</article></main>
+</body>
+</html>
--- /dev/null
+<!DOCTYPE html>
+<html lang="en">
+<head>
+ <meta charset="UTF-8" />
+ <title>inspect_annotations | My Python Project</title>
+ <link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Source+Sans+Pro:400,400i,600,600i%7CSource+Code+Pro:400,400i,600" />
+ <link rel="stylesheet" href="m-dark+documentation.compiled.css" />
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+</head>
+<body>
+<header><nav id="navigation">
+ <div class="m-container">
+ <div class="m-row">
+ <a href="index.html" id="m-navbar-brand" class="m-col-t-8 m-col-m-none m-left-m">My Python Project</a>
+ </div>
+ </div>
+</nav></header>
+<main><article>
+ <div class="m-container m-container-inflatable">
+ <div class="m-row">
+ <div class="m-col-l-10 m-push-l-1">
+ <h1>
+ inspect_annotations <span class="m-thin">module</span>
+ </h1>
+ <div class="m-block m-default">
+ <h3>Contents</h3>
+ <ul>
+ <li>
+ Reference
+ <ul>
+ <li><a href="#classes">Classes</a></li>
+ <li><a href="#functions">Functions</a></li>
+ <li><a href="#data">Data</a></li>
+ </ul>
+ </li>
+ </ul>
+ </div>
+ <section id="classes">
+ <h2><a href="#classes">Classes</a></h2>
+ <dl class="m-doc">
+ <dt>class <a href="inspect_annotations.Foo.html" class="m-doc">Foo</a></dt>
+ <dd>A class with properties</dd>
+ </dl>
+ </section>
+ <section id="functions">
+ <h2><a href="#functions">Functions</a></h2>
+ <dl class="m-doc">
+ <dt>
+ <span class="m-doc-wrap-bumper">def <a href="" class="m-doc-self">annotated_positional_keyword</a>(</span><span class="m-doc-wrap">bar = False,<span class="m-text m-dim"> *,</span>
+ foo: str,
+ **kwargs)</span>
+ </dt>
+ <dd>Function with explicitly delimited keyword args and type annotations</dd>
+ <dt>
+ <span class="m-doc-wrap-bumper">def <a href="" class="m-doc-self">annotation</a>(</span><span class="m-doc-wrap">param: typing.List[int],
+ another: bool,
+ third: str = 'hello') -> inspect_annotations.Foo</span>
+ </dt>
+ <dd>An annotated function</dd>
+ <dt>
+ <span class="m-doc-wrap-bumper">def <a href="" class="m-doc-self">args_kwargs</a>(</span><span class="m-doc-wrap">a, b, *args, **kwargs)</span>
+ </dt>
+ <dd>Function with args and kwargs</dd>
+ <dt>
+ <span class="m-doc-wrap-bumper">def <a href="" class="m-doc-self">no_annotation</a>(</span><span class="m-doc-wrap">a, b, z)</span>
+ </dt>
+ <dd>Non-annotated function</dd>
+ <dt>
+ <span class="m-doc-wrap-bumper">def <a href="" class="m-doc-self">no_annotation_default_param</a>(</span><span class="m-doc-wrap">param,
+ another,
+ third = 'hello')</span>
+ </dt>
+ <dd>Non-annotated function</dd>
+ <dt>
+ <span class="m-doc-wrap-bumper">def <a href="" class="m-doc-self">partial_annotation</a>(</span><span class="m-doc-wrap">foo,
+ param: typing.Tuple[int, int],
+ unannotated,
+ cls: inspect_annotations.Foo)</span>
+ </dt>
+ <dd>Partially annotated function</dd>
+ <dt>
+ <span class="m-doc-wrap-bumper">def <a href="" class="m-doc-self">positional_keyword</a>(</span><span class="m-doc-wrap">positional_kw,<span class="m-text m-dim"> *,</span> kw_only)</span>
+ </dt>
+ <dd>Function with explicitly delimited keyword args</dd>
+ </dl>
+ </section>
+ <section id="data">
+ <h2><a href="#data">Data</a></h2>
+ <dl class="m-doc">
+ <dt>
+ <a href="" class="m-doc-self">ANNOTATED_VAR</a>: typing.Tuple[bool, str] = (False, 'No.')
+ </dt>
+ <dd></dd>
+ <dt>
+ <a href="" class="m-doc-self">UNANNOTATED_VAR</a> = 3.45
+ </dt>
+ <dd></dd>
+ </dl>
+ </section>
+ </div>
+ </div>
+ </div>
+</article></main>
+</body>
+</html>
--- /dev/null
+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.')
--- /dev/null
+<!DOCTYPE html>
+<html lang="en">
+<head>
+ <meta charset="UTF-8" />
+ <title>math | My Python Project</title>
+ <link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Source+Sans+Pro:400,400i,600,600i%7CSource+Code+Pro:400,400i,600" />
+ <link rel="stylesheet" href="m-dark+documentation.compiled.css" />
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+</head>
+<body>
+<header><nav id="navigation">
+ <div class="m-container">
+ <div class="m-row">
+ <a href="index.html" id="m-navbar-brand" class="m-col-t-8 m-col-m-none m-left-m">My Python Project</a>
+ </div>
+ </div>
+</nav></header>
+<main><article>
+ <div class="m-container m-container-inflatable">
+ <div class="m-row">
+ <div class="m-col-l-10 m-push-l-1">
+ <h1>
+ math <span class="m-thin">module</span>
+ </h1>
+ <p>This module is always available. It provides access to the
+mathematical functions defined by the C standard.</p>
+ <div class="m-block m-default">
+ <h3>Contents</h3>
+ <ul>
+ <li>
+ Reference
+ <ul>
+ <li><a href="#functions">Functions</a></li>
+ </ul>
+ </li>
+ </ul>
+ </div>
+ <section id="functions">
+ <h2><a href="#functions">Functions</a></h2>
+ <dl class="m-doc">
+ <dt>
+ <span class="m-doc-wrap-bumper">def <a href="" class="m-doc-self">pow</a>(</span><span class="m-doc-wrap">x, y<span class="m-text m-dim">, /</span>)</span>
+ </dt>
+ <dd>Return x**y (x to the power of y).</dd>
+ <dt>
+ <span class="m-doc-wrap-bumper">def <a href="" class="m-doc-self">log</a>(</span><span class="m-doc-wrap">...)</span>
+ </dt>
+ <dd>log(x, [base=math.e])
+Return the logarithm of x to the given base.</dd>
+ </dl>
+ </section>
+ </div>
+ </div>
+ </div>
+</article></main>
+</body>
+</html>
--- /dev/null
+<!DOCTYPE html>
+<html lang="en">
+<head>
+ <meta charset="UTF-8" />
+ <title>inspect_string.Foo | My Python Project</title>
+ <link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Source+Sans+Pro:400,400i,600,600i%7CSource+Code+Pro:400,400i,600" />
+ <link rel="stylesheet" href="m-dark+documentation.compiled.css" />
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+</head>
+<body>
+<header><nav id="navigation">
+ <div class="m-container">
+ <div class="m-row">
+ <a href="index.html" id="m-navbar-brand" class="m-col-t-8 m-col-m-none m-left-m">My Python Project</a>
+ </div>
+ </div>
+</nav></header>
+<main><article>
+ <div class="m-container m-container-inflatable">
+ <div class="m-row">
+ <div class="m-col-l-10 m-push-l-1">
+ <h1>
+ <span class="m-breadcrumb"><a href="inspect_string.html">inspect_string</a>.<wbr/></span>Foo <span class="m-thin">class</span>
+ </h1>
+ <p>The foo class</p>
+ <div class="m-block m-default">
+ <h3>Contents</h3>
+ <ul>
+ <li>
+ Reference
+ <ul>
+ <li><a href="#classes">Classes</a></li>
+ <li><a href="#classmethods">Class methods</a></li>
+ <li><a href="#staticmethods">Static methods</a></li>
+ <li><a href="#methods">Methods</a></li>
+ <li><a href="#properties">Properties</a></li>
+ <li><a href="#data">Data</a></li>
+ </ul>
+ </li>
+ </ul>
+ </div>
+ <section id="classes">
+ <h2><a href="#classes">Classes</a></h2>
+ <dl class="m-doc">
+ <dt>class <a href="inspect_string.Foo.Subclass.html" class="m-doc">Subclass</a></dt>
+ <dd>A subclass of Foo</dd>
+ </dl>
+ </section>
+ <section id="classmethods">
+ <h2><a href="#classmethods">Class methods</a></h2>
+ <dl class="m-doc">
+ <dt>
+ <span class="m-doc-wrap-bumper">def <a href="" class="m-doc-self">func_on_class</a>(</span><span class="m-doc-wrap">a)</span>
+ </dt>
+ <dd>A class method</dd>
+ </dl>
+ </section>
+ <section id="staticmethods">
+ <h2><a href="#staticmethods">Static methods</a></h2>
+ <dl class="m-doc">
+ <dt>
+ <span class="m-doc-wrap-bumper">def <a href="" class="m-doc-self">static_func</a>(</span><span class="m-doc-wrap">a)</span>
+ </dt>
+ <dd>A static method</dd>
+ </dl>
+ </section>
+ <section id="methods">
+ <h2><a href="#methods">Methods</a></h2>
+ <dl class="m-doc">
+ <dt>
+ <span class="m-doc-wrap-bumper">def <a href="" class="m-doc-self">func</a>(</span><span class="m-doc-wrap">self, a, b)</span>
+ </dt>
+ <dd>A method</dd>
+ </dl>
+ </section>
+ <section id="properties">
+ <h2><a href="#properties">Properties</a></h2>
+ <dl class="m-doc">
+ <dt>
+ <a href="" class="m-doc-self">a_property</a> <span class="m-label m-flat m-warning">get</span>
+ </dt>
+ <dd>A property</dd>
+ <dt>
+ <a href="" class="m-doc-self">deletable_property</a> <span class="m-label m-flat m-warning">get del</span>
+ </dt>
+ <dd>Deletable property</dd>
+ <dt>
+ <a href="" class="m-doc-self">writable_property</a> <span class="m-label m-flat m-success">get set</span>
+ </dt>
+ <dd>Writable property</dd>
+ </dl>
+ </section>
+ <section id="data">
+ <h2><a href="#data">Data</a></h2>
+ <dl class="m-doc">
+ <dt>
+ <a href="" class="m-doc-self">A_DATA</a> = 'BOO'
+ </dt>
+ <dd></dd>
+ </dl>
+ </section>
+ </div>
+ </div>
+ </div>
+</article></main>
+</body>
+</html>
--- /dev/null
+<!DOCTYPE html>
+<html lang="en">
+<head>
+ <meta charset="UTF-8" />
+ <title>inspect_string.Specials | My Python Project</title>
+ <link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Source+Sans+Pro:400,400i,600,600i%7CSource+Code+Pro:400,400i,600" />
+ <link rel="stylesheet" href="m-dark+documentation.compiled.css" />
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+</head>
+<body>
+<header><nav id="navigation">
+ <div class="m-container">
+ <div class="m-row">
+ <a href="index.html" id="m-navbar-brand" class="m-col-t-8 m-col-m-none m-left-m">My Python Project</a>
+ </div>
+ </div>
+</nav></header>
+<main><article>
+ <div class="m-container m-container-inflatable">
+ <div class="m-row">
+ <div class="m-col-l-10 m-push-l-1">
+ <h1>
+ <span class="m-breadcrumb"><a href="inspect_string.html">inspect_string</a>.<wbr/></span>Specials <span class="m-thin">class</span>
+ </h1>
+ <p>Special class members</p>
+ <div class="m-block m-default">
+ <h3>Contents</h3>
+ <ul>
+ <li>
+ Reference
+ <ul>
+ <li><a href="#dunder-methods">Special methods</a></li>
+ </ul>
+ </li>
+ </ul>
+ </div>
+ <section id="dunder-methods">
+ <h2><a href="#dunder-methods">Special methods</a></h2>
+ <dl class="m-doc">
+ <dt>
+ <span class="m-doc-wrap-bumper">def <a href="" class="m-doc-self">__add__</a>(</span><span class="m-doc-wrap">self, other)</span>
+ </dt>
+ <dd>Add a thing</dd>
+ <dt>
+ <span class="m-doc-wrap-bumper">def <a href="" class="m-doc-self">__and__</a>(</span><span class="m-doc-wrap">self, other)</span>
+ </dt>
+ <dd></dd>
+ <dt>
+ <span class="m-doc-wrap-bumper">def <a href="" class="m-doc-self">__init__</a>(</span><span class="m-doc-wrap">self)</span>
+ </dt>
+ <dd>The constructor</dd>
+ </dl>
+ </section>
+ </div>
+ </div>
+ </div>
+</article></main>
+</body>
+</html>
--- /dev/null
+<!DOCTYPE html>
+<html lang="en">
+<head>
+ <meta charset="UTF-8" />
+ <title>inspect_string.another_module | My Python Project</title>
+ <link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Source+Sans+Pro:400,400i,600,600i%7CSource+Code+Pro:400,400i,600" />
+ <link rel="stylesheet" href="m-dark+documentation.compiled.css" />
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+</head>
+<body>
+<header><nav id="navigation">
+ <div class="m-container">
+ <div class="m-row">
+ <a href="index.html" id="m-navbar-brand" class="m-col-t-8 m-col-m-none m-left-m">My Python Project</a>
+ </div>
+ </div>
+</nav></header>
+<main><article>
+ <div class="m-container m-container-inflatable">
+ <div class="m-row">
+ <div class="m-col-l-10 m-push-l-1">
+ <h1>
+ <span class="m-breadcrumb"><a href="inspect_string.html">inspect_string</a>.<wbr/></span>another_module <span class="m-thin">module</span>
+ </h1>
+ <p>Another module</p>
+ </div>
+ </div>
+ </div>
+</article></main>
+</body>
+</html>
--- /dev/null
+<!DOCTYPE html>
+<html lang="en">
+<head>
+ <meta charset="UTF-8" />
+ <title>inspect_string | My Python Project</title>
+ <link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Source+Sans+Pro:400,400i,600,600i%7CSource+Code+Pro:400,400i,600" />
+ <link rel="stylesheet" href="m-dark+documentation.compiled.css" />
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+</head>
+<body>
+<header><nav id="navigation">
+ <div class="m-container">
+ <div class="m-row">
+ <a href="index.html" id="m-navbar-brand" class="m-col-t-8 m-col-m-none m-left-m">My Python Project</a>
+ </div>
+ </div>
+</nav></header>
+<main><article>
+ <div class="m-container m-container-inflatable">
+ <div class="m-row">
+ <div class="m-col-l-10 m-push-l-1">
+ <h1>
+ inspect_string <span class="m-thin">module</span>
+ </h1>
+ <p>A module</p>
+ <div class="m-block m-default">
+ <h3>Contents</h3>
+ <ul>
+ <li>
+ Reference
+ <ul>
+ <li><a href="#packages">Modules</a></li>
+ <li><a href="#classes">Classes</a></li>
+ <li><a href="#functions">Functions</a></li>
+ <li><a href="#data">Data</a></li>
+ </ul>
+ </li>
+ </ul>
+ </div>
+ <section id="namespaces">
+ <h2><a href="#namespaces">Modules</a></h2>
+ <dl class="m-doc">
+ <dt>module <a href="inspect_string.another_module.html" class="m-doc">another_module</a></dt>
+ <dd>Another module</dd>
+ <dt>module <a href="inspect_string.subpackage.html" class="m-doc">subpackage</a></dt>
+ <dd>A subpackage</dd>
+ </dl>
+ </section>
+ <section id="classes">
+ <h2><a href="#classes">Classes</a></h2>
+ <dl class="m-doc">
+ <dt>class <a href="inspect_string.Foo.html" class="m-doc">Foo</a></dt>
+ <dd>The foo class</dd>
+ <dt>class <a href="inspect_string.Specials.html" class="m-doc">Specials</a></dt>
+ <dd>Special class members</dd>
+ </dl>
+ </section>
+ <section id="functions">
+ <h2><a href="#functions">Functions</a></h2>
+ <dl class="m-doc">
+ <dt>
+ <span class="m-doc-wrap-bumper">def <a href="" class="m-doc-self">function</a>(</span><span class="m-doc-wrap">)</span>
+ </dt>
+ <dd>A function</dd>
+ </dl>
+ </section>
+ <section id="data">
+ <h2><a href="#data">Data</a></h2>
+ <dl class="m-doc">
+ <dt>
+ <a href="" class="m-doc-self">A_CONSTANT</a> = 3.24
+ </dt>
+ <dd></dd>
+ <dt>
+ <a href="" class="m-doc-self">foo</a>
+ </dt>
+ <dd></dd>
+ </dl>
+ </section>
+ </div>
+ </div>
+ </div>
+</article></main>
+</body>
+</html>
--- /dev/null
+"""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()
--- /dev/null
+"""Private module."""
--- /dev/null
+"""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
--- /dev/null
+"""A subpackage"""
--- /dev/null
+<!DOCTYPE html>
+<html lang="en">
+<head>
+ <meta charset="UTF-8" />
+ <title>A project | A project is cool</title>
+ <link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Source+Sans+Pro:400,400i,600,600i%7CSource+Code+Pro:400,400i,600" />
+ <link rel="stylesheet" href="m-dark+documentation.compiled.css" />
+ <link rel="icon" href="favicon-light.png" type="image/png" />
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+ <meta name="theme-color" content="#00ffff" />
+ <!-- this is extra in the header -->
+ <!-- and more, indented -->
+ <!-- yes. -->
+</head>
+<body>
+<header><nav id="navigation">
+ <div class="m-container">
+ <div class="m-row">
+ <a href="index.html" id="m-navbar-brand" class="m-col-t-8 m-col-m-none m-left-m">A project <span class="m-thin">is cool</span></a>
+ <div class="m-col-t-4 m-hide-m m-text-right m-nopadr">
+ <a href="#search" class="m-doc-search-icon" title="Search" onclick="return showSearch()"><svg style="height: 0.9rem;" viewBox="0 0 16 16">
+ <path d="m6 0c-3.3144 0-6 2.6856-6 6 0 3.3144 2.6856 6 6 6 1.4858 0 2.8463-0.54083 3.8945-1.4355-0.0164 0.33797 0.14734 0.75854 0.5 1.1504l3.2227 3.7891c0.55185 0.6139 1.4517 0.66544 2.002 0.11524 0.55022-0.55022 0.49866-1.4501-0.11524-2.002l-3.7891-3.2246c-0.39184-0.35266-0.81242-0.51469-1.1504-0.5 0.89472-1.0482 1.4355-2.4088 1.4355-3.8945 0-3.3128-2.6856-5.998-6-5.998zm0 1.5625a4.4375 4.4375 0 0 1 4.4375 4.4375 4.4375 4.4375 0 0 1-4.4375 4.4375 4.4375 4.4375 0 0 1-4.4375-4.4375 4.4375 4.4375 0 0 1 4.4375-4.4375z"/>
+ </svg></a>
+ <a id="m-navbar-show" href="#navigation" title="Show navigation"></a>
+ <a id="m-navbar-hide" href="#" title="Hide navigation"></a>
+ </div>
+ <div id="m-navbar-collapse" class="m-col-t-12 m-show-m m-col-m-none m-right-m">
+ <div class="m-row">
+ <ol class="m-col-t-12 m-col-m-none">
+ <li>
+ <a href="pages.html">Pages</a>
+ <ol>
+ <li><a href="getting-started.html">Getting started</a></li>
+ <li><a href="troubleshooting.html">Troubleshooting</a></li>
+ </ol>
+ </li>
+ <li><a href="modules.html">Modules</a></li>
+ </ol>
+ <ol class="m-col-t-6 m-col-m-none" start="3">
+ <li><a href="classes.html">Classes</a></li>
+ <li>
+ <a href="https://github.com/mosra/m.css">GitHub</a>
+ <ol>
+ <li><a href="about.html">About</a></li>
+ </ol>
+ </li>
+ <li class="m-show-m"><a href="#search" class="m-doc-search-icon" title="Search" onclick="return showSearch()"><svg style="height: 0.9rem;" viewBox="0 0 16 16">
+ <path d="m6 0c-3.3144 0-6 2.6856-6 6 0 3.3144 2.6856 6 6 6 1.4858 0 2.8463-0.54083 3.8945-1.4355-0.0164 0.33797 0.14734 0.75854 0.5 1.1504l3.2227 3.7891c0.55185 0.6139 1.4517 0.66544 2.002 0.11524 0.55022-0.55022 0.49866-1.4501-0.11524-2.002l-3.7891-3.2246c-0.39184-0.35266-0.81242-0.51469-1.1504-0.5 0.89472-1.0482 1.4355-2.4088 1.4355-3.8945 0-3.3128-2.6856-5.998-6-5.998zm0 1.5625a4.4375 4.4375 0 0 1 4.4375 4.4375 4.4375 4.4375 0 0 1-4.4375 4.4375 4.4375 4.4375 0 0 1-4.4375-4.4375 4.4375 4.4375 0 0 1 4.4375-4.4375z"/>
+ </svg></a></li>
+ </ol>
+ </div>
+ </div>
+ </div>
+ </div>
+</nav></header>
+<main><article>
+ <div class="m-container m-container-inflatable">
+ <div class="m-row">
+ <div class="m-col-l-10 m-push-l-1">
+ <p><a href="index.html">A self link</a></p>
+ <h1>
+ A project <span class="m-thin">module</span>
+ </h1>
+ </div>
+ </div>
+ </div>
+</article></main>
+<div class="m-doc-search" id="search">
+ <a href="#!" onclick="return hideSearch()"></a>
+ <div class="m-container">
+ <div class="m-row">
+ <div class="m-col-m-8 m-push-m-2">
+ <div class="m-doc-search-header m-text m-small">
+ <div><span class="m-label m-default">Tab</span> / <span class="m-label m-default">T</span> to search, <span class="m-label m-default">Esc</span> to close</div>
+ <div id="search-symbolcount">…</div>
+ </div>
+ <div class="m-doc-search-content">
+ <form>
+ <input type="search" name="q" id="search-input" placeholder="Loading …" disabled="disabled" autofocus="autofocus" autocomplete="off" spellcheck="false" />
+ </form>
+ <noscript class="m-text m-danger m-text-center">Unlike everything else in the docs, the search functionality <em>requires</em> JavaScript. Enable it or <a href="https://google.com/search?q=site:mcss.mosra.cz+{}">use an external search engine</a>.</noscript>
+ <div id="search-help" class="m-text m-dim m-text-center">
+ <p>Some <em>help</em>.
+ On multiple lines.</p>
+ </div>
+ <div id="search-notfound" class="m-text m-warning m-text-center">Sorry, nothing was found.<br />Maybe try a full-text <a href="#" id="search-external" data-search-engine="https://google.com/search?q=site:mcss.mosra.cz+{}">search with external engine</a>?</div>
+ <ul id="search-results"></ul>
+ </div>
+ </div>
+ </div>
+ </div>
+</div>
+<script src="search.js"></script>
+<script src="searchdata.js" async="async"></script>
+<footer><nav>
+ <div class="m-container">
+ <div class="m-row">
+ <div class="m-col-l-10 m-push-l-1">
+ <p>This beautiful thing is done thanks to
+ <a href="https://mcss.mosra.cz">m.css</a>.</p>
+ </div>
+ </div>
+ </div>
+</nav></footer>
+</body>
+</html>
--- /dev/null
+#
+# This file is part of m.css.
+#
+# Copyright © 2017, 2018, 2019 Vladimír Vondruš <mosra@centrum.cz>
+#
+# 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'))
--- /dev/null
+#
+# This file is part of m.css.
+#
+# Copyright © 2017, 2018, 2019 Vladimír Vondruš <mosra@centrum.cz>
+#
+# 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 <https://mcss.mosra.cz>`_.",
+ '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':
+"""<!-- this is extra in the header -->
+ <!-- and more, indented -->
+<!-- yes. -->""",
+ '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')))
python: 3.4
env:
- WITH_THEME=ON
- - WITH_DOXYGEN=OFF
+ - WITH_DOCUMENTATION=OFF
- WITH_NODE=OFF
- JOBID=py34
- language: python
- graphviz
env:
- WITH_THEME=ON
- - WITH_DOXYGEN=OFF
+ - WITH_DOCUMENTATION=OFF
- WITH_NODE=OFF
- JOBID=py35
- language: python
- graphviz
env:
- WITH_THEME=ON
- - WITH_DOXYGEN=ON
+ - WITH_DOCUMENTATION=ON
- WITH_NODE=OFF
- TRAVIS_BROKEN_MATPLOTLIB_SEED=YES
- JOBID=py36
node_js: 8
env:
- WITH_THEME=OFF
- - WITH_DOXYGEN=OFF
+ - WITH_DOCUMENTATION=OFF
- WITH_NODE=ON
- JOBID=js
- 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
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
('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'),
('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/'),