diff --git a/.gitignore b/.gitignore index 92c1e95c8..d47b1e991 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,5 @@ jupyterhub_cookie_secret *.crt *.key doc/build +doc/source/api +doc/source/remoteappmanager_help.txt \ No newline at end of file diff --git a/COPYING.rst b/LICENSE.rst similarity index 100% rename from COPYING.rst rename to LICENSE.rst diff --git a/README.rst b/README.rst index 7dad75b59..f160fbb8e 100644 --- a/README.rst +++ b/README.rst @@ -1,21 +1,24 @@ -Remote Application Manager --------------------------- +Simphony-remote +=============== .. image:: https://readthedocs.org/projects/simphony-remote/badge/?version=latest :target: http://simphony-remote.readthedocs.io/en/latest/?badge=latest :alt: Documentation Status -This software is developed under the SimPhoNy project, an EU-project funded by -the 7th Framework Programme (Project number 604005) under the call -NMP.2013.1.4-1: "Development of an integrated multi-scale modelling environment -for nanomaterials and systems by design". +The Simphony-remote is web service that allows users to start and work with simphony enabled environments remotely. -The package provides an executable replacement of jupyterhub-singleuser, and -provides management capabilities for docker images and containers. It is spawn -by the jupyterhub server in response to a login. +Key provided features: -License -------- + - Isolated working environments using docker containers. + - No install remote access through a web browser. + - Sharing of working sessions. + - Based on community supported open source initiatives (JupyterHub) -This software is released under the terms of the BSD license. See the -`COPYING.rst `_ for license terms. + +Acknowledgments +--------------- + +This software is developed under the SimPhoNy project, an EU-project +funded by the 7th Framework Programme (Project number 604005) under +the call NMP.2013.1.4-1: "Development of an integrated multi-scale +modelling environment for nanomaterials and systems by design". diff --git a/doc/Makefile b/doc/Makefile index 22a433c81..231fba10b 100644 --- a/doc/Makefile +++ b/doc/Makefile @@ -49,6 +49,7 @@ help: .PHONY: clean clean: rm -rf $(BUILDDIR)/* + rm -rf source/api .PHONY: html html: diff --git a/doc/make.bat b/doc/make.bat index 27b79f401..bb50358f6 100644 --- a/doc/make.bat +++ b/doc/make.bat @@ -48,6 +48,7 @@ if "%1" == "help" ( if "%1" == "clean" ( for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i del /q /s %BUILDDIR%\* + rmdir /q /s source\api goto end ) diff --git a/doc/source/_extensions/__init__.py b/doc/source/_extensions/__init__.py new file mode 100644 index 000000000..98cedd8dc --- /dev/null +++ b/doc/source/_extensions/__init__.py @@ -0,0 +1,587 @@ +# -*- coding: utf-8 -*- +""" + sphinx.ext.autosummary + ~~~~~~~~~~~~~~~~~~~~~~ + + Sphinx extension that adds an autosummary:: directive, which can be + used to generate function/method/attribute/etc. summary lists, similar + to those output eg. by Epydoc and other API doc generation tools. + + An :autolink: role is also provided. + + autosummary directive + --------------------- + + The autosummary directive has the form:: + + .. autosummary:: + :nosignatures: + :toctree: generated/ + + module.function_1 + module.function_2 + ... + + and it generates an output table (containing signatures, optionally) + + ======================== ============================================= + module.function_1(args) Summary line from the docstring of function_1 + module.function_2(args) Summary line from the docstring + ... + ======================== ============================================= + + If the :toctree: option is specified, files matching the function names + are inserted to the toctree with the given prefix: + + generated/module.function_1 + generated/module.function_2 + ... + + Note: The file names contain the module:: or currentmodule:: prefixes. + + .. seealso:: autosummary_generate.py + + + autolink role + ------------- + + The autolink role functions as ``:obj:`` when the name referred can be + resolved to a Python object, and otherwise it becomes simple emphasis. + This can be used as the default role to make links 'smart'. + + :copyright: Copyright 2007-2016 by the Sphinx team, see AUTHORS. + :license: BSD, see LICENSE for details. +""" + +import os +import re +import sys +import inspect +import posixpath +from types import ModuleType + +from six import text_type +from docutils.parsers.rst import directives +from docutils.statemachine import ViewList +from docutils import nodes + +import sphinx +from sphinx import addnodes +from sphinx.util.compat import Directive +from sphinx.pycode import ModuleAnalyzer, PycodeError +from sphinx.ext.autodoc import Options + + +# -- autosummary_toc node ------------------------------------------------------ + +class autosummary_toc(nodes.comment): + pass + + +def process_autosummary_toc(app, doctree): + """Insert items described in autosummary:: to the TOC tree, but do + not generate the toctree:: list. + """ + env = app.builder.env + crawled = {} + + def crawl_toc(node, depth=1): + crawled[node] = True + for j, subnode in enumerate(node): + try: + if (isinstance(subnode, autosummary_toc) and + isinstance(subnode[0], addnodes.toctree)): + env.note_toctree(env.docname, subnode[0]) + continue + except IndexError: + continue + if not isinstance(subnode, nodes.section): + continue + if subnode not in crawled: + crawl_toc(subnode, depth+1) + crawl_toc(doctree) + + +def autosummary_toc_visit_html(self, node): + """Hide autosummary toctree list in HTML output.""" + raise nodes.SkipNode + + +def autosummary_noop(self, node): + pass + + +# -- autosummary_table node ---------------------------------------------------- + +class autosummary_table(nodes.comment): + pass + + +def autosummary_table_visit_html(self, node): + """Make the first column of the table non-breaking.""" + try: + tbody = node[0][0][-1] + for row in tbody: + col1_entry = row[0] + par = col1_entry[0] + for j, subnode in enumerate(list(par)): + if isinstance(subnode, nodes.Text): + new_text = text_type(subnode.astext()) + new_text = new_text.replace(u" ", u"\u00a0") + par[j] = nodes.Text(new_text) + except IndexError: + pass + + +# -- autodoc integration ------------------------------------------------------- + +class FakeDirective: + env = {} + genopt = Options() + + +def get_documenter(obj, parent): + """Get an autodoc.Documenter class suitable for documenting the given + object. + + *obj* is the Python object to be documented, and *parent* is an + another Python object (e.g. a module or a class) to which *obj* + belongs to. + """ + from sphinx.ext.autodoc import AutoDirective, DataDocumenter, \ + ModuleDocumenter + + if inspect.ismodule(obj): + # ModuleDocumenter.can_document_member always returns False + return ModuleDocumenter + + # Construct a fake documenter for *parent* + if parent is not None: + parent_doc_cls = get_documenter(parent, None) + else: + parent_doc_cls = ModuleDocumenter + + if hasattr(parent, '__name__'): + parent_doc = parent_doc_cls(FakeDirective(), parent.__name__) + else: + parent_doc = parent_doc_cls(FakeDirective(), "") + + # Get the corrent documenter class for *obj* + classes = [cls for cls in AutoDirective._registry.values() + if cls.can_document_member(obj, '', False, parent_doc)] + if classes: + classes.sort(key=lambda cls: cls.priority) + return classes[-1] + else: + return DataDocumenter + + +# -- .. autosummary:: ---------------------------------------------------------- + +class Autosummary(Directive): + """ + Pretty table containing short signatures and summaries of functions etc. + + autosummary can also optionally generate a hidden toctree:: node. + """ + + required_arguments = 0 + optional_arguments = 0 + final_argument_whitespace = False + has_content = True + option_spec = { + 'toctree': directives.unchanged, + 'nosignatures': directives.flag, + 'template': directives.unchanged, + } + + def warn(self, msg): + self.warnings.append(self.state.document.reporter.warning( + msg, line=self.lineno)) + + def run(self): + self.env = env = self.state.document.settings.env + self.genopt = Options() + self.warnings = [] + self.result = ViewList() + + names = [x.strip().split()[0] for x in self.content + if x.strip() and re.search(r'^[~a-zA-Z_]', x.strip()[0])] + items = self.get_items(names) + nodes = self.get_table(items) + + if 'toctree' in self.options: + dirname = posixpath.dirname(env.docname) + + tree_prefix = self.options['toctree'].strip() + docnames = [] + for name, sig, summary, real_name in items: + docname = posixpath.join(tree_prefix, real_name) + docname = posixpath.normpath(posixpath.join(dirname, docname)) + if docname not in env.found_docs: + self.warn('toctree references unknown document %r' + % docname) + docnames.append(docname) + + tocnode = addnodes.toctree() + tocnode['includefiles'] = docnames + tocnode['entries'] = [(None, docn) for docn in docnames] + tocnode['maxdepth'] = -1 + tocnode['glob'] = None + + tocnode = autosummary_toc('', '', tocnode) + nodes.append(tocnode) + + return self.warnings + nodes + + def get_items(self, names): + """Try to import the given names, and return a list of + ``[(name, signature, summary_string, real_name), ...]``. + """ + env = self.state.document.settings.env + + prefixes = get_import_prefixes_from_env(env) + + items = [] + + max_item_chars = 50 + + for name in names: + display_name = name + if name.startswith('~'): + name = name[1:] + display_name = name.split('.')[-1] + + try: + real_name, obj, parent, modname = import_by_name(name, prefixes=prefixes) + except ImportError: + self.warn('failed to import %s' % name) + items.append((name, '', '', name)) + continue + + self.result = ViewList() # initialize for each documenter + full_name = real_name + if not isinstance(obj, ModuleType): + # give explicitly separated module name, so that members + # of inner classes can be documented + full_name = modname + '::' + full_name[len(modname)+1:] + # NB. using full_name here is important, since Documenters + # handle module prefixes slightly differently + documenter = get_documenter(obj, parent)(self, full_name) + if not documenter.parse_name(): + self.warn('failed to parse name %s' % real_name) + items.append((display_name, '', '', real_name)) + continue + if not documenter.import_object(): + self.warn('failed to import object %s' % real_name) + items.append((display_name, '', '', real_name)) + continue + if documenter.options.members and not documenter.check_module(): + continue + + # try to also get a source code analyzer for attribute docs + try: + documenter.analyzer = ModuleAnalyzer.for_module( + documenter.get_real_modname()) + # parse right now, to get PycodeErrors on parsing (results will + # be cached anyway) + documenter.analyzer.find_attr_docs() + except PycodeError as err: + documenter.env.app.debug( + '[autodoc] module analyzer failed: %s', err) + # no source file -- e.g. for builtin and C modules + documenter.analyzer = None + + # -- Grab the signature + + sig = documenter.format_signature() + if not sig: + sig = '' + else: + max_chars = max(10, max_item_chars - len(display_name)) + sig = mangle_signature(sig, max_chars=max_chars) + sig = sig.replace('*', r'\*') + + # -- Grab the summary + + documenter.add_content(None) + doc = list(documenter.process_doc([self.result.data])) + + while doc and not doc[0].strip(): + doc.pop(0) + + # If there's a blank line, then we can assume the first sentence / + # paragraph has ended, so anything after shouldn't be part of the + # summary + for i, piece in enumerate(doc): + if not piece.strip(): + doc = doc[:i] + break + + # Try to find the "first sentence", which may span multiple lines + m = re.search(r"^([A-Z].*?\.)(?:\s|$)", " ".join(doc).strip()) + if m: + summary = m.group(1).strip() + elif doc: + summary = doc[0].strip() + else: + summary = '' + + items.append((display_name, sig, summary, real_name)) + + return items + + def get_table(self, items): + """Generate a proper list of table nodes for autosummary:: directive. + + *items* is a list produced by :meth:`get_items`. + """ + table_spec = addnodes.tabular_col_spec() + table_spec['spec'] = 'p{0.5\linewidth}p{0.5\linewidth}' + + table = autosummary_table('') + real_table = nodes.table('', classes=['longtable']) + table.append(real_table) + group = nodes.tgroup('', cols=2) + real_table.append(group) + group.append(nodes.colspec('', colwidth=10)) + group.append(nodes.colspec('', colwidth=90)) + body = nodes.tbody('') + group.append(body) + + def append_row(*column_texts): + row = nodes.row('') + for text in column_texts: + node = nodes.paragraph('') + vl = ViewList() + vl.append(text, '') + self.state.nested_parse(vl, 0, node) + try: + if isinstance(node[0], nodes.paragraph): + node = node[0] + except IndexError: + pass + row.append(nodes.entry('', node)) + body.append(row) + + for name, sig, summary, real_name in items: + qualifier = 'obj' + if 'nosignatures' not in self.options: + col1 = ':%s:`%s <%s>`\ %s' % (qualifier, name, real_name, sig) + else: + col1 = ':%s:`%s <%s>`' % (qualifier, name, real_name) + col2 = summary + append_row(col1, col2) + + return [table_spec, table] + + +def mangle_signature(sig, max_chars=30): + """Reformat a function signature to a more compact form.""" + s = re.sub(r"^\((.*)\)$", r"\1", sig).strip() + + # Strip strings (which can contain things that confuse the code below) + s = re.sub(r"\\\\", "", s) + s = re.sub(r"\\'", "", s) + s = re.sub(r"'[^']*'", "", s) + + # Parse the signature to arguments + options + args = [] + opts = [] + + opt_re = re.compile(r"^(.*, |)([a-zA-Z0-9_*]+)=") + while s: + m = opt_re.search(s) + if not m: + # The rest are arguments + args = s.split(', ') + break + + opts.insert(0, m.group(2)) + s = m.group(1)[:-2] + + # Produce a more compact signature + sig = limited_join(", ", args, max_chars=max_chars-2) + if opts: + if not sig: + sig = "[%s]" % limited_join(", ", opts, max_chars=max_chars-4) + elif len(sig) < max_chars - 4 - 2 - 3: + sig += "[, %s]" % limited_join(", ", opts, + max_chars=max_chars-len(sig)-4-2) + + return u"(%s)" % sig + + +def limited_join(sep, items, max_chars=30, overflow_marker="..."): + """Join a number of strings to one, limiting the length to *max_chars*. + + If the string overflows this limit, replace the last fitting item by + *overflow_marker*. + + Returns: joined_string + """ + full_str = sep.join(items) + if len(full_str) < max_chars: + return full_str + + n_chars = 0 + n_items = 0 + for j, item in enumerate(items): + n_chars += len(item) + len(sep) + if n_chars < max_chars - len(overflow_marker): + n_items += 1 + else: + break + + return sep.join(list(items[:n_items]) + [overflow_marker]) + + +# -- Importing items ----------------------------------------------------------- + +def get_import_prefixes_from_env(env): + """ + Obtain current Python import prefixes (for `import_by_name`) + from ``document.env`` + """ + prefixes = [None] + + currmodule = env.ref_context.get('py:module') + if currmodule: + prefixes.insert(0, currmodule) + + currclass = env.ref_context.get('py:class') + if currclass: + if currmodule: + prefixes.insert(0, currmodule + "." + currclass) + else: + prefixes.insert(0, currclass) + + return prefixes + + +def import_by_name(name, prefixes=[None]): + """Import a Python object that has the given *name*, under one of the + *prefixes*. The first name that succeeds is used. + """ + tried = [] + for prefix in prefixes: + try: + if prefix: + prefixed_name = '.'.join([prefix, name]) + else: + prefixed_name = name + obj, parent, modname = _import_by_name(prefixed_name) + return prefixed_name, obj, parent, modname + except ImportError: + tried.append(prefixed_name) + raise ImportError('no module named %s' % ' or '.join(tried)) + + +def _import_by_name(name): + """Import a Python object given its full name.""" + try: + name_parts = name.split('.') + + # try first interpret `name` as MODNAME.OBJ + modname = '.'.join(name_parts[:-1]) + if modname: + try: + __import__(modname) + mod = sys.modules[modname] + return getattr(mod, name_parts[-1]), mod, modname + except (ImportError, IndexError, AttributeError): + pass + + # ... then as MODNAME, MODNAME.OBJ1, MODNAME.OBJ1.OBJ2, ... + last_j = 0 + modname = None + for j in reversed(range(1, len(name_parts)+1)): + last_j = j + modname = '.'.join(name_parts[:j]) + try: + __import__(modname) + except ImportError: + continue + if modname in sys.modules: + break + + if last_j < len(name_parts): + parent = None + obj = sys.modules[modname] + for obj_name in name_parts[last_j:]: + parent = obj + obj = getattr(obj, obj_name) + return obj, parent, modname + else: + return sys.modules[modname], None, modname + except (ValueError, ImportError, AttributeError, KeyError) as e: + raise ImportError(*e.args) + + +# -- :autolink: (smart default role) ------------------------------------------- + +def autolink_role(typ, rawtext, etext, lineno, inliner, + options={}, content=[]): + """Smart linking role. + + Expands to ':obj:`text`' if `text` is an object that can be imported; + otherwise expands to '*text*'. + """ + env = inliner.document.settings.env + r = env.get_domain('py').role('obj')( + 'obj', rawtext, etext, lineno, inliner, options, content) + pnode = r[0][0] + + prefixes = get_import_prefixes_from_env(env) + try: + name, obj, parent, modname = import_by_name(pnode['reftarget'], prefixes) + except ImportError: + content = pnode[0] + r[0][0] = nodes.emphasis(rawtext, content[0].astext(), + classes=content['classes']) + return r + + +def process_generate_options(app): + genfiles = app.config.autosummary_generate + + if genfiles and not hasattr(genfiles, '__len__'): + env = app.builder.env + genfiles = [env.doc2path(x, base=None) for x in env.found_docs + if os.path.isfile(env.doc2path(x))] + + if not genfiles: + return + + from .generate import generate_autosummary_docs + + ext = app.config.source_suffix[0] + genfiles = [genfile + (not genfile.endswith(ext) and ext or '') + for genfile in genfiles] + + generate_autosummary_docs(genfiles, builder=app.builder, + warn=app.warn, info=app.info, suffix=ext, + base_path=app.srcdir) + + +def setup(app): + # I need autodoc + app.setup_extension('sphinx.ext.autodoc') + app.add_node(autosummary_toc, + html=(autosummary_toc_visit_html, autosummary_noop), + latex=(autosummary_noop, autosummary_noop), + text=(autosummary_noop, autosummary_noop), + man=(autosummary_noop, autosummary_noop), + texinfo=(autosummary_noop, autosummary_noop)) + app.add_node(autosummary_table, + html=(autosummary_table_visit_html, autosummary_noop), + latex=(autosummary_noop, autosummary_noop), + text=(autosummary_noop, autosummary_noop), + man=(autosummary_noop, autosummary_noop), + texinfo=(autosummary_noop, autosummary_noop)) + app.add_directive('autosummary', Autosummary) + app.add_role('autolink', autolink_role) + app.connect('doctree-read', process_autosummary_toc) + app.connect('builder-inited', process_generate_options) + app.add_config_value('autosummary_generate', [], True, [bool]) + return {'version': sphinx.__display_version__, 'parallel_read_safe': True} diff --git a/doc/source/_extensions/generate.py b/doc/source/_extensions/generate.py new file mode 100644 index 000000000..ba511f0ab --- /dev/null +++ b/doc/source/_extensions/generate.py @@ -0,0 +1,348 @@ +# -*- coding: utf-8 -*- +""" + sphinx.ext.autosummary.generate + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + Usable as a library or script to generate automatic RST source files for + items referred to in autosummary:: directives. + + Each generated RST file contains a single auto*:: directive which + extracts the docstring of the referred item. + + Example Makefile rule:: + + generate: + sphinx-autogen -o source/generated source/*.rst + + :copyright: Copyright 2007-2016 by the Sphinx team, see AUTHORS. + :license: BSD, see LICENSE for details. +""" +from __future__ import print_function + +import os +import re +import sys +import pydoc +import optparse +import codecs + +from jinja2 import FileSystemLoader, TemplateNotFound +from jinja2.sandbox import SandboxedEnvironment + +from sphinx import package_dir +from . import import_by_name, get_documenter +from sphinx.jinja2glue import BuiltinTemplateLoader +from sphinx.util.osutil import ensuredir +from sphinx.util.inspect import safe_getattr + +# Add documenters to AutoDirective registry +from sphinx.ext.autodoc import add_documenter, \ + ModuleDocumenter, ClassDocumenter, ExceptionDocumenter, DataDocumenter, \ + FunctionDocumenter, MethodDocumenter, AttributeDocumenter, \ + InstanceAttributeDocumenter +add_documenter(ModuleDocumenter) +add_documenter(ClassDocumenter) +add_documenter(ExceptionDocumenter) +add_documenter(DataDocumenter) +add_documenter(FunctionDocumenter) +add_documenter(MethodDocumenter) +add_documenter(AttributeDocumenter) +add_documenter(InstanceAttributeDocumenter) + + +def main(argv=sys.argv): + usage = """%prog [OPTIONS] SOURCEFILE ...""" + p = optparse.OptionParser(usage.strip()) + p.add_option("-o", "--output-dir", action="store", type="string", + dest="output_dir", default=None, + help="Directory to place all output in") + p.add_option("-s", "--suffix", action="store", type="string", + dest="suffix", default="rst", + help="Default suffix for files (default: %default)") + p.add_option("-t", "--templates", action="store", type="string", + dest="templates", default=None, + help="Custom template directory (default: %default)") + options, args = p.parse_args(argv[1:]) + + if len(args) < 1: + p.error('no input files given') + + generate_autosummary_docs(args, options.output_dir, + "." + options.suffix, + template_dir=options.templates) + + +def _simple_info(msg): + print(msg) + + +def _simple_warn(msg): + print('WARNING: ' + msg, file=sys.stderr) + + +# -- Generating output --------------------------------------------------------- + +def generate_autosummary_docs(sources, output_dir=None, suffix='.rst', + warn=_simple_warn, info=_simple_info, + base_path=None, builder=None, template_dir=None): + + showed_sources = list(sorted(sources)) + if len(showed_sources) > 20: + showed_sources = showed_sources[:10] + ['...'] + showed_sources[-10:] + info('[autosummary] generating autosummary for: %s' % + ', '.join(showed_sources)) + + if output_dir: + info('[autosummary] writing to %s' % output_dir) + + if base_path is not None: + sources = [os.path.join(base_path, filename) for filename in sources] + + # create our own templating environment + template_dirs = [os.path.join(package_dir, 'ext', + 'autosummary', 'templates')] + if builder is not None: + # allow the user to override the templates + template_loader = BuiltinTemplateLoader() + template_loader.init(builder, dirs=template_dirs) + else: + if template_dir: + template_dirs.insert(0, template_dir) + template_loader = FileSystemLoader(template_dirs) + template_env = SandboxedEnvironment(loader=template_loader) + + # read + items = find_autosummary_in_files(sources) + + # keep track of new files + new_files = [] + + # write + for name, path, template_name in sorted(set(items), key=str): + if path is None: + # The corresponding autosummary:: directive did not have + # a :toctree: option + continue + + path = output_dir or os.path.abspath(path) + ensuredir(path) + + try: + name, obj, parent, mod_name = import_by_name(name) + except ImportError as e: + warn('[autosummary] failed to import %r: %s' % (name, e)) + continue + + fn = os.path.join(path, name + suffix) + + # skip it if it exists + if os.path.isfile(fn): + continue + + new_files.append(fn) + + with open(fn, 'w') as f: + doc = get_documenter(obj, parent) + + if template_name is not None: + template = template_env.get_template(template_name) + else: + try: + template = template_env.get_template('autosummary/%s.rst' + % doc.objtype) + except TemplateNotFound: + template = template_env.get_template('autosummary/base.rst') + + # Patched get_members according to + # http://stackoverflow.com/questions/25405110/sphinx-autosummary-with-toctree-also-lists-imported-members/25460763#25460763 + def get_members(obj, typ, include_public=[], imported=False): + items = [] + for name in dir(obj): + try: + obj_name = safe_getattr(obj, name) + documenter = get_documenter(obj_name, obj) + except AttributeError: + continue + if documenter.objtype == typ: + try: + cond = ( + imported or + obj_name.__module__ == obj.__name__ + ) + except AttributeError: + cond = True + if cond: + items.append(name) + public = [x for x in items + if x in include_public or not x.startswith('_')] + return public, items + + ns = {} + + if doc.objtype == 'module': + ns['members'] = dir(obj) + ns['functions'], ns['all_functions'] = \ + get_members(obj, 'function') + ns['classes'], ns['all_classes'] = \ + get_members(obj, 'class') + ns['exceptions'], ns['all_exceptions'] = \ + get_members(obj, 'exception') + elif doc.objtype == 'class': + ns['members'] = dir(obj) + ns['methods'], ns['all_methods'] = \ + get_members(obj, 'method', ['__init__']) + ns['attributes'], ns['all_attributes'] = \ + get_members(obj, 'attribute') + + parts = name.split('.') + if doc.objtype in ('method', 'attribute'): + mod_name = '.'.join(parts[:-2]) + cls_name = parts[-2] + obj_name = '.'.join(parts[-2:]) + ns['class'] = cls_name + else: + mod_name, obj_name = '.'.join(parts[:-1]), parts[-1] + + ns['fullname'] = name + ns['module'] = mod_name + ns['objname'] = obj_name + ns['name'] = parts[-1] + + ns['objtype'] = doc.objtype + ns['underline'] = len(name) * '=' + + rendered = template.render(**ns) + f.write(rendered) + + # descend recursively to new files + if new_files: + generate_autosummary_docs(new_files, output_dir=output_dir, + suffix=suffix, warn=warn, info=info, + base_path=base_path, builder=builder, + template_dir=template_dir) + + +# -- Finding documented entries in files --------------------------------------- + +def find_autosummary_in_files(filenames): + """Find out what items are documented in source/*.rst. + + See `find_autosummary_in_lines`. + """ + documented = [] + for filename in filenames: + with codecs.open(filename, 'r', encoding='utf-8', + errors='ignore') as f: + lines = f.read().splitlines() + documented.extend(find_autosummary_in_lines(lines, + filename=filename)) + return documented + + +def find_autosummary_in_docstring(name, module=None, filename=None): + """Find out what items are documented in the given object's docstring. + + See `find_autosummary_in_lines`. + """ + try: + real_name, obj, parent, modname = import_by_name(name) + lines = pydoc.getdoc(obj).splitlines() + return find_autosummary_in_lines(lines, module=name, filename=filename) + except AttributeError: + pass + except ImportError as e: + print("Failed to import '%s': %s" % (name, e)) + except SystemExit as e: + print("Failed to import '%s'; the module executes module level " + "statement and it might call sys.exit()." % name) + return [] + + +def find_autosummary_in_lines(lines, module=None, filename=None): + """Find out what items appear in autosummary:: directives in the + given lines. + + Returns a list of (name, toctree, template) where *name* is a name + of an object and *toctree* the :toctree: path of the corresponding + autosummary directive (relative to the root of the file name), and + *template* the value of the :template: option. *toctree* and + *template* ``None`` if the directive does not have the + corresponding options set. + """ + autosummary_re = re.compile(r'^(\s*)\.\.\s+autosummary::\s*') + automodule_re = re.compile( + r'^\s*\.\.\s+automodule::\s*([A-Za-z0-9_.]+)\s*$') + module_re = re.compile( + r'^\s*\.\.\s+(current)?module::\s*([a-zA-Z0-9_.]+)\s*$') + autosummary_item_re = re.compile(r'^\s+(~?[_a-zA-Z][a-zA-Z0-9_.]*)\s*.*?') + toctree_arg_re = re.compile(r'^\s+:toctree:\s*(.*?)\s*$') + template_arg_re = re.compile(r'^\s+:template:\s*(.*?)\s*$') + + documented = [] + + toctree = None + template = None + current_module = module + in_autosummary = False + base_indent = "" + + for line in lines: + if in_autosummary: + m = toctree_arg_re.match(line) + if m: + toctree = m.group(1) + if filename: + toctree = os.path.join(os.path.dirname(filename), + toctree) + continue + + m = template_arg_re.match(line) + if m: + template = m.group(1).strip() + continue + + if line.strip().startswith(':'): + continue # skip options + + m = autosummary_item_re.match(line) + if m: + name = m.group(1).strip() + if name.startswith('~'): + name = name[1:] + if current_module and \ + not name.startswith(current_module + '.'): + name = "%s.%s" % (current_module, name) + documented.append((name, toctree, template)) + continue + + if not line.strip() or line.startswith(base_indent + " "): + continue + + in_autosummary = False + + m = autosummary_re.match(line) + if m: + in_autosummary = True + base_indent = m.group(1) + toctree = None + template = None + continue + + m = automodule_re.search(line) + if m: + current_module = m.group(1).strip() + # recurse into the automodule docstring + documented.extend(find_autosummary_in_docstring( + current_module, filename=filename)) + continue + + m = module_re.match(line) + if m: + current_module = m.group(2) + continue + + return documented + + +if __name__ == '__main__': + main() diff --git a/doc/source/_templates/module_template.rst b/doc/source/_templates/module_template.rst new file mode 100644 index 000000000..9f18aeeaf --- /dev/null +++ b/doc/source/_templates/module_template.rst @@ -0,0 +1,61 @@ +{{ name }} +{{ underline }} + +.. currentmodule:: {{ fullname }} +.. module:: {{ fullname }} + +{% block functions %} +{% if functions %} +.. rubric:: Functions + +.. autosummary:: +{% for item in functions %} + {{ item }} +{%- endfor %} + +| +| + +{% for item in functions %} +.. autofunction:: {{ item }} +{%- endfor %} +{% endif %} +{% endblock %} +{% block classes %} +{% if classes %} +.. rubric:: Classes + +.. autosummary:: + +{% for item in classes %} + {{ item }} +{%- endfor %} + +| +| + +{% for item in classes %} +.. autoclass:: {{ item }} +{%- endfor %} +{% endif %} +{% endblock %} + +{% block exceptions %} +{% if exceptions %} +.. rubric:: Exceptions + +.. autosummary:: + +{% for item in exceptions %} + {{ item }} +{%- endfor %} + +| +| + +{% for item in exceptions %} +.. autoclass:: {{ item }} + +{%- endfor %} +{% endif %} +{% endblock %} diff --git a/doc/source/api.rst b/doc/source/api.rst deleted file mode 100644 index a26d1201e..000000000 --- a/doc/source/api.rst +++ /dev/null @@ -1,7 +0,0 @@ -API -=== - -.. toctree:: - :maxdepth: 4 - - api/remoteappmanager diff --git a/doc/source/api/remoteappmanager.cli.remoteappdb.rst b/doc/source/api/remoteappmanager.cli.remoteappdb.rst deleted file mode 100644 index a73c511d1..000000000 --- a/doc/source/api/remoteappmanager.cli.remoteappdb.rst +++ /dev/null @@ -1,10 +0,0 @@ -remoteappmanager.cli.remoteappdb package -======================================== - -Module contents ---------------- - -.. automodule:: remoteappmanager.cli.remoteappdb - :members: - :undoc-members: - :show-inheritance: diff --git a/doc/source/api/remoteappmanager.cli.remoteapprest.rst b/doc/source/api/remoteappmanager.cli.remoteapprest.rst deleted file mode 100644 index 736b8c8d4..000000000 --- a/doc/source/api/remoteappmanager.cli.remoteapprest.rst +++ /dev/null @@ -1,10 +0,0 @@ -remoteappmanager.cli.remoteapprest package -========================================== - -Module contents ---------------- - -.. automodule:: remoteappmanager.cli.remoteapprest - :members: - :undoc-members: - :show-inheritance: diff --git a/doc/source/api/remoteappmanager.cli.rst b/doc/source/api/remoteappmanager.cli.rst deleted file mode 100644 index 86d1cd916..000000000 --- a/doc/source/api/remoteappmanager.cli.rst +++ /dev/null @@ -1,18 +0,0 @@ -remoteappmanager.cli package -============================ - -Subpackages ------------ - -.. toctree:: - - remoteappmanager.cli.remoteappdb - remoteappmanager.cli.remoteapprest - -Module contents ---------------- - -.. automodule:: remoteappmanager.cli - :members: - :undoc-members: - :show-inheritance: diff --git a/doc/source/api/remoteappmanager.db.rst b/doc/source/api/remoteappmanager.db.rst deleted file mode 100644 index e33719b6c..000000000 --- a/doc/source/api/remoteappmanager.db.rst +++ /dev/null @@ -1,42 +0,0 @@ -remoteappmanager.db package -=========================== - -Submodules ----------- - -remoteappmanager.db.csv_db module ---------------------------------- - -.. automodule:: remoteappmanager.db.csv_db - :members: - :special-members: __init__ - :undoc-members: - :show-inheritance: - -remoteappmanager.db.interfaces module -------------------------------------- - -.. automodule:: remoteappmanager.db.interfaces - :members: - :special-members: __init__ - :undoc-members: - :show-inheritance: - -remoteappmanager.db.orm module ------------------------------- - -.. automodule:: remoteappmanager.db.orm - :members: - :special-members: __init__ - :undoc-members: - :show-inheritance: - - -Module contents ---------------- - -.. automodule:: remoteappmanager.db - :members: - :special-members: __init__ - :undoc-members: - :show-inheritance: diff --git a/doc/source/api/remoteappmanager.docker.rst b/doc/source/api/remoteappmanager.docker.rst deleted file mode 100644 index 80644a4cb..000000000 --- a/doc/source/api/remoteappmanager.docker.rst +++ /dev/null @@ -1,54 +0,0 @@ -remoteappmanager.docker package -=============================== - -Submodules ----------- - -remoteappmanager.docker.async_docker_client module --------------------------------------------------- - -.. automodule:: remoteappmanager.docker.async_docker_client - :members: - :undoc-members: - :show-inheritance: - -remoteappmanager.docker.container module ----------------------------------------- - -.. automodule:: remoteappmanager.docker.container - :members: - :undoc-members: - :show-inheritance: - -remoteappmanager.docker.container_manager module ------------------------------------------------- - -.. automodule:: remoteappmanager.docker.container_manager - :members: - :undoc-members: - :show-inheritance: - -remoteappmanager.docker.docker_labels module --------------------------------------------- - -.. automodule:: remoteappmanager.docker.docker_labels - :members: - :undoc-members: - :show-inheritance: - -remoteappmanager.docker.image module ------------------------------------- - -.. automodule:: remoteappmanager.docker.image - :members: - :undoc-members: - :show-inheritance: - - -Module contents ---------------- - -.. automodule:: remoteappmanager.docker - :members: - :undoc-members: - :show-inheritance: diff --git a/doc/source/api/remoteappmanager.handlers.rst b/doc/source/api/remoteappmanager.handlers.rst deleted file mode 100644 index c94faaf6a..000000000 --- a/doc/source/api/remoteappmanager.handlers.rst +++ /dev/null @@ -1,38 +0,0 @@ -remoteappmanager.handlers package -================================= - -Submodules ----------- - -remoteappmanager.handlers.api module ------------------------------------- - -.. automodule:: remoteappmanager.handlers.api - :members: - :undoc-members: - :show-inheritance: - -remoteappmanager.handlers.base_handler module ---------------------------------------------- - -.. automodule:: remoteappmanager.handlers.base_handler - :members: - :undoc-members: - :show-inheritance: - -remoteappmanager.handlers.home_handler module ---------------------------------------------- - -.. automodule:: remoteappmanager.handlers.home_handler - :members: - :undoc-members: - :show-inheritance: - - -Module contents ---------------- - -.. automodule:: remoteappmanager.handlers - :members: - :undoc-members: - :show-inheritance: diff --git a/doc/source/api/remoteappmanager.logging.rst b/doc/source/api/remoteappmanager.logging.rst deleted file mode 100644 index 2d362ce34..000000000 --- a/doc/source/api/remoteappmanager.logging.rst +++ /dev/null @@ -1,22 +0,0 @@ -remoteappmanager.logging package -================================ - -Submodules ----------- - -remoteappmanager.logging.logging_mixin module ---------------------------------------------- - -.. automodule:: remoteappmanager.logging.logging_mixin - :members: - :undoc-members: - :show-inheritance: - - -Module contents ---------------- - -.. automodule:: remoteappmanager.logging - :members: - :undoc-members: - :show-inheritance: diff --git a/doc/source/api/remoteappmanager.rest.http.rst b/doc/source/api/remoteappmanager.rest.http.rst deleted file mode 100644 index d045864d9..000000000 --- a/doc/source/api/remoteappmanager.rest.http.rst +++ /dev/null @@ -1,30 +0,0 @@ -remoteappmanager.rest.http package -================================== - -Submodules ----------- - -remoteappmanager.rest.http.httpstatus module --------------------------------------------- - -.. automodule:: remoteappmanager.rest.http.httpstatus - :members: - :undoc-members: - :show-inheritance: - -remoteappmanager.rest.http.payloaded_http_error module ------------------------------------------------------- - -.. automodule:: remoteappmanager.rest.http.payloaded_http_error - :members: - :undoc-members: - :show-inheritance: - - -Module contents ---------------- - -.. automodule:: remoteappmanager.rest.http - :members: - :undoc-members: - :show-inheritance: diff --git a/doc/source/api/remoteappmanager.rest.rst b/doc/source/api/remoteappmanager.rest.rst deleted file mode 100644 index 5615c69a1..000000000 --- a/doc/source/api/remoteappmanager.rest.rst +++ /dev/null @@ -1,53 +0,0 @@ -remoteappmanager.rest package -============================= - -Subpackages ------------ - -.. toctree:: - - remoteappmanager.rest.http - -Submodules ----------- - -remoteappmanager.rest.exceptions module ---------------------------------------- - -.. automodule:: remoteappmanager.rest.exceptions - :members: - :undoc-members: - :show-inheritance: - -remoteappmanager.rest.registry module -------------------------------------- - -.. automodule:: remoteappmanager.rest.registry - :members: - :undoc-members: - :show-inheritance: - -remoteappmanager.rest.resource module -------------------------------------- - -.. automodule:: remoteappmanager.rest.resource - :members: - :undoc-members: - :show-inheritance: - -remoteappmanager.rest.rest_handler module ------------------------------------------ - -.. automodule:: remoteappmanager.rest.rest_handler - :members: - :undoc-members: - :show-inheritance: - - -Module contents ---------------- - -.. automodule:: remoteappmanager.rest - :members: - :undoc-members: - :show-inheritance: diff --git a/doc/source/api/remoteappmanager.restresources.rst b/doc/source/api/remoteappmanager.restresources.rst deleted file mode 100644 index ead031892..000000000 --- a/doc/source/api/remoteappmanager.restresources.rst +++ /dev/null @@ -1,30 +0,0 @@ -remoteappmanager.restresources package -====================================== - -Submodules ----------- - -remoteappmanager.restresources.application module -------------------------------------------------- - -.. automodule:: remoteappmanager.restresources.application - :members: - :undoc-members: - :show-inheritance: - -remoteappmanager.restresources.container module ------------------------------------------------ - -.. automodule:: remoteappmanager.restresources.container - :members: - :undoc-members: - :show-inheritance: - - -Module contents ---------------- - -.. automodule:: remoteappmanager.restresources - :members: - :undoc-members: - :show-inheritance: diff --git a/doc/source/api/remoteappmanager.rst b/doc/source/api/remoteappmanager.rst deleted file mode 100644 index 4fc61bd24..000000000 --- a/doc/source/api/remoteappmanager.rst +++ /dev/null @@ -1,116 +0,0 @@ -remoteappmanager package -======================== - -Subpackages ------------ - -.. toctree:: - - remoteappmanager.cli - remoteappmanager.db - remoteappmanager.docker - remoteappmanager.handlers - remoteappmanager.logging - remoteappmanager.rest - remoteappmanager.restresources - remoteappmanager.services - -Submodules ----------- - -remoteappmanager.application module ------------------------------------ - -.. automodule:: remoteappmanager.application - :members: - :undoc-members: - :show-inheritance: - -remoteappmanager.auth module ----------------------------- - -.. automodule:: remoteappmanager.auth - :members: - :undoc-members: - :show-inheritance: - -remoteappmanager.command_line_config module -------------------------------------------- - -.. automodule:: remoteappmanager.command_line_config - :members: - :undoc-members: - :show-inheritance: - -remoteappmanager.file_config module ------------------------------------ - -.. automodule:: remoteappmanager.file_config - :members: - :undoc-members: - :show-inheritance: - -remoteappmanager.jinja2_adapters module ---------------------------------------- - -.. automodule:: remoteappmanager.jinja2_adapters - :members: - :undoc-members: - :show-inheritance: - -remoteappmanager.netutils module --------------------------------- - -.. automodule:: remoteappmanager.netutils - :members: - :undoc-members: - :show-inheritance: - -remoteappmanager.paths module ------------------------------ - -.. automodule:: remoteappmanager.paths - :members: - :undoc-members: - :show-inheritance: - -remoteappmanager.spawners module --------------------------------- - -.. automodule:: remoteappmanager.spawners - :members: - :undoc-members: - :show-inheritance: - -remoteappmanager.traitlets module ---------------------------------- - -.. automodule:: remoteappmanager.traitlets - :members: - :undoc-members: - :show-inheritance: - -remoteappmanager.user module ----------------------------- - -.. automodule:: remoteappmanager.user - :members: - :undoc-members: - :show-inheritance: - -remoteappmanager.utils module ------------------------------ - -.. automodule:: remoteappmanager.utils - :members: - :undoc-members: - :show-inheritance: - - -Module contents ---------------- - -.. automodule:: remoteappmanager - :members: - :undoc-members: - :show-inheritance: diff --git a/doc/source/api/remoteappmanager.services.rst b/doc/source/api/remoteappmanager.services.rst deleted file mode 100644 index d3ef14ea8..000000000 --- a/doc/source/api/remoteappmanager.services.rst +++ /dev/null @@ -1,30 +0,0 @@ -remoteappmanager.services package -================================= - -Submodules ----------- - -remoteappmanager.services.hub module ------------------------------------- - -.. automodule:: remoteappmanager.services.hub - :members: - :undoc-members: - :show-inheritance: - -remoteappmanager.services.reverse_proxy module ----------------------------------------------- - -.. automodule:: remoteappmanager.services.reverse_proxy - :members: - :undoc-members: - :show-inheritance: - - -Module contents ---------------- - -.. automodule:: remoteappmanager.services - :members: - :undoc-members: - :show-inheritance: diff --git a/doc/source/conf.py b/doc/source/conf.py index 61b673618..de5bb5da9 100644 --- a/doc/source/conf.py +++ b/doc/source/conf.py @@ -19,7 +19,11 @@ # import os import sys -sys.path.append(os.path.abspath('../../remoteappmanager')) +sys.path.append(os.path.abspath('../../')) +sys.path.append(os.path.abspath('.')) + +from mock_missing import mock_modules +mock_modules() from remoteappmanager import __version__, MAJOR, MINOR from remoteappmanager.command_line_config import CommandLineConfig @@ -41,6 +45,10 @@ 'sphinx.ext.coverage', 'sphinx.ext.viewcode', 'traitlet_documenter', + # patched autosummary for issue + # https://github.com/sphinx-doc/sphinx/issues/1061 + '_extensions' + #'sphinx.ext.autosummary' ] # Add any paths that contain templates here, relative to this directory. @@ -442,7 +450,24 @@ with open(HELP_FILE_PATH, 'w') as fh: for name, traitlet in sorted(_config.traits().items()): - print('--{name:<30} {help}'.format(name=name, help=traitlet.help), - file=fh) - + print( + '--{name:<30} {help}'.format( + name=name, help=traitlet.help), file=fh) del _config + + +autodoc_member_order = 'source' +autoclass_content = 'both' +autodoc_default_flags = [ + 'show-inheritance', 'members', 'undoc-members'] +autosummary_generate = True + +# Patch sphinx to 1.4.x to suppress warning about nonlocal image URI +import sphinx.environment +from docutils.utils import get_source_line + +def _warn_node(self, msg, node, **kwargs): + if not msg.startswith('nonlocal image URI found:'): + self._warnfunc(msg, '%s:%s' % get_source_line(node), **kwargs) + +sphinx.environment.BuildEnvironment.warn_node = _warn_node diff --git a/doc/source/developer.rst b/doc/source/developer.rst index 9fadf573d..3b74ddd68 100644 --- a/doc/source/developer.rst +++ b/doc/source/developer.rst @@ -1,7 +1,57 @@ -Developer documentation -======================= +API reference +============= -.. toctree:: - :maxdepth: 1 - api +RemoteAppManager +---------------- + +The main tornado web application that manages the containers (docker applications) +for each user. + +.. autosummary:: + :toctree: api + :template: module_template.rst + + remoteappmanager.application + remoteappmanager.auth + remoteappmanager.command_line_config + remoteappmanager.file_config + remoteappmanager.jinja2_adapters + remoteappmanager.netutils + remoteappmanager.spawners + remoteappmanager.traitlets + remoteappmanager.user + remoteappmanager.utils + remoteappmanager.cli.remoteappdb.__main__ + remoteappmanager.cli.remoteapprest.__main__ + remoteappmanager.db.csv_db + remoteappmanager.db.interfaces + remoteappmanager.db.orm + remoteappmanager.docker.async_docker_client + remoteappmanager.docker.container + remoteappmanager.docker.container_manager + remoteappmanager.docker.image + remoteappmanager.handlers.base_handler + remoteappmanager.handlers.home_handler + remoteappmanager.logging.logging_mixin + remoteappmanager.restresources.application + remoteappmanager.restresources.container + remoteappmanager.services.hub + remoteappmanager.services.reverse_proxy + + +REST tornado +------------ + +A generic implementation of Rest APIs using tornado. + +.. autosummary:: + :toctree: api + :template: module_template.rst + + remoteappmanager.rest.exceptions + remoteappmanager.rest.registry + remoteappmanager.rest.resource + remoteappmanager.rest.rest_handler + remoteappmanager.rest.http.payloaded_http_error + diff --git a/doc/source/index.rst b/doc/source/index.rst index 03a9a063a..38e424f1d 100644 --- a/doc/source/index.rst +++ b/doc/source/index.rst @@ -1,17 +1,8 @@ -Simphony-remote documentation -============================= +.. include:: ../../README.rst -The Simphony-remote is web service that allows users to start and work with -simphony enabled environments remotely. -Key provided features: - - - Isolated working environments using docker containers. - - No install remote access through a web browser. - - Sharing of working sessions. - - Based on community supported open source initiatives (JupyterHub) - -.. note:: More to come... +Contents +-------- .. toctree:: :maxdepth: 1 @@ -22,4 +13,8 @@ Key provided features: design developer troubleshooting - license + +License +------- + +.. include:: ../../LICENSE.rst diff --git a/doc/source/license.rst b/doc/source/license.rst deleted file mode 100644 index 226f0dc36..000000000 --- a/doc/source/license.rst +++ /dev/null @@ -1,4 +0,0 @@ -License -======= - -.. include:: ../../COPYING.rst diff --git a/doc/source/mock_missing.py b/doc/source/mock_missing.py new file mode 100644 index 000000000..55fade065 --- /dev/null +++ b/doc/source/mock_missing.py @@ -0,0 +1,86 @@ +#------------------------------------------------------------------------------ +# +# Copyright (c) 2015, Enthought, Inc. +# All rights reserved. +# +# This software is provided without warranty under the terms of the BSD +# license included in LICENSE.txt and may be redistributed only +# under the conditions described in the aforementioned license. The license +# is also available online at http://www.enthought.com/licenses/BSD.txt +# +# Thanks for using Enthought open source! +# +#------------------------------------------------------------------------------ +import sys + + +def mock_modules(): + """ Mock missing modules if necessary """ + + MOCK_MODULES = [] + MOCK_TYPES = [] + + try: + import pwd + except ImportError: + MOCK_MODULES = ['pwd'] + MOCK_TYPES = [] + else: + del pwd + + try: + import grp + except ImportError: + MOCK_MODULES.append('grp') + else: + del grp + + try: + import pamela + except ImportError: + MOCK_MODULES.append('pamela') + else: + del pamela + + TYPES = { + mock_type: type(mock_type, bases, {'__module__': path}) + for path, mock_type, bases in MOCK_TYPES} + + class DocMock(object): + + def __init__(self, *args, **kwds): + if '__doc_mocked_name__' in kwds: + self.__docmock_name__ = kwds['__docmocked_name__'] + else: + self.__docmock_name__ = 'Unknown' + + def __getattr__(self, name): + if name in ('__file__', '__path__'): + return '/dev/null' + elif name == '__all__': + return [] + else: + return TYPES.get(name, DocMock(__docmock_name__=name)) + + def __call__(self, *args, **kwards): + return DocMock() + + def __iter__(self): + return self + + def __next__(self): + raise StopIteration() + + def next(self): + raise StopIteration() + + @property + def __name__(self): + return self.__docmock_name__ + + def __repr__(self): + return ''.format(self.__name__) + + sys.modules.update( + (mod_name, DocMock(mocked_name=mod_name)) for mod_name in MOCK_MODULES) + print('mocking modules {} and types {}'.format(MOCK_MODULES, MOCK_TYPES)) diff --git a/doc/source/troubleshooting.rst b/doc/source/troubleshooting.rst index 24fc89785..af883f0a3 100644 --- a/doc/source/troubleshooting.rst +++ b/doc/source/troubleshooting.rst @@ -1,6 +1,9 @@ Troubleshoot ------------ +.. toctree:: + :includehidden: + .. include:: troubleshoot/database.rst diff --git a/tox.ini b/tox.ini index 9881b3e05..575dd648d 100644 --- a/tox.ini +++ b/tox.ini @@ -11,4 +11,4 @@ commands=python -m tornado.testing discover -s tests -t . -v passenv = * [flake8] -exclude = build/*,venv/*,doc/source/conf.py,tests/utils.py,selenium_tests/* +exclude = build/*,venv/*,doc/source/*,tests/utils.py,selenium_tests/*