1#!/usr/bin/env python
2"""
3Script to generate documentation for command line utilities
4"""
5import os
6from os.path import join as pjoin
7import re
8from subprocess import Popen, PIPE, CalledProcessError
9import sys
10import importlib
11import inspect
12
13# version comparison
14from distutils.version import LooseVersion as V
15
16# List of workflows to ignore
17SKIP_WORKFLOWS_LIST = ('Workflow', 'CombinedWorkflow')
18
19
20def sh3(cmd):
21    """
22    Execute command in a subshell, return stdout, stderr
23    If anything appears in stderr, print it out to sys.stderr
24
25    https://github.com/scikit-image/scikit-image/blob/master/doc/gh-pages.py
26
27    Copyright (C) 2011, the scikit-image team All rights reserved.
28
29    Redistribution and use in source and binary forms, with or without
30    modification, are permitted provided that the following conditions are met:
31
32    Redistributions of source code must retain the above copyright notice,
33    this list of conditions and the following disclaimer.
34    Redistributions in binary form must reproduce the above copyright notice,
35    this list of conditions and the following disclaimer in the documentation
36    and/or other materials provided with the distribution.
37    Neither the name of skimage nor the names of its contributors may be used
38    to endorse or promote products derived from this software without specific
39    prior written permission.
40
41    THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
42    IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
43    OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
44    IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
45    INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
46    BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
47    USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
48    THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
49    (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
50    THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
51    """
52    p = Popen(cmd, stdout=PIPE, stderr=PIPE, shell=True)
53    out, err = p.communicate()
54    retcode = p.returncode
55    if retcode:
56        raise CalledProcessError(retcode, cmd)
57    else:
58        return out.rstrip(), err.rstrip()
59
60
61def abort(error):
62    print('*WARNING* Command line API documentation not generated: %s' % error)
63    exit()
64
65
66def get_help_string(class_obj):
67    # return inspect.getdoc(class_obj.run)
68    try:
69        ia_module = importlib.import_module("dipy.workflows.base")
70        parser = ia_module.IntrospectiveArgumentParser()
71        parser.add_workflow(class_obj())
72    except Exception as e:
73        abort("Error on {0}: {1}".format(class_obj.__name__, e))
74
75    return parser.format_help()
76
77
78def format_title(text):
79    text = text.title()
80    line = '-' * len(text)
81    return '{0}\n{1}\n\n'.format(text, line)
82
83
84if __name__ == '__main__':
85    # package name: Eg: dipy
86    package = sys.argv[1]
87    # directory in which the generated rst files will be saved
88    outdir = sys.argv[2]
89
90    try:
91        __import__(package)
92    except ImportError:
93        abort("Cannot import " + package)
94
95    module = sys.modules[package]
96
97    # Check that the source version is equal to the installed
98    # version. If the versions mismatch the API documentation sources
99    # are not (re)generated. This avoids automatic generation of documentation
100    # for older or newer versions if such versions are installed on the system.
101
102    installed_version = V(module.__version__)
103
104    info_file = pjoin('..', package, 'info.py')
105    info_lines = open(info_file).readlines()
106    source_version = '.'.join(
107        [v.split('=')[1].strip(" '\n.")
108         for v in info_lines
109         if re.match('^_version_(major|minor|micro|extra)', v)])
110    print('***', source_version)
111
112    if source_version != installed_version:
113        abort("Installed version does not match source version")
114
115    # generate docs
116    command_list = []
117
118    workflows_folder = pjoin('..', 'bin')
119    workflow_module = importlib.import_module("dipy.workflows.workflow")
120
121    workflow_flist = [os.path.abspath(pjoin(workflows_folder, f))
122                      for f in os.listdir(workflows_folder)
123                      if os.path.isfile(pjoin(workflows_folder, f)) and
124                      f.lower().startswith("dipy_")]
125
126    workflow_flist = sorted(workflow_flist)
127    workflow_desc = {}
128    # We get all workflows class obj in a dictionary
129    for path_file in os.listdir(pjoin('..', 'dipy', 'workflows')):
130        module_name = inspect.getmodulename(path_file)
131        if module_name is None:
132            continue
133
134        module = importlib.import_module("dipy.workflows." + module_name)
135        members = inspect.getmembers(module)
136        d_wkflw = {name: {"module": obj, "help": get_help_string(obj)}
137                   for name, obj in members
138                   if inspect.isclass(obj) and
139                   issubclass(obj, workflow_module.Workflow) and
140                   name not in SKIP_WORKFLOWS_LIST
141                   }
142
143        workflow_desc.update(d_wkflw)
144
145    cmd_list = []
146    for fpath in workflow_flist:
147        fname = os.path.basename(fpath)
148        with open(fpath) as file_object:
149            flow_name = set(re.findall(r"[A-Z]\w+Flow", file_object.read(),
150                                       re.X | re.M))
151
152        if not flow_name or len(flow_name) != 1:
153            continue
154
155        flow_name = list(flow_name)[-1]
156        print("Generating docs for: {0} ({1})".format(fname, flow_name))
157        out_fname = fname + ".rst"
158        with open(pjoin(outdir, out_fname), "w") as fp:
159            dashes = "=" * len(fname)
160            fp.write("\n{0}\n{1}\n{0}\n\n".format(dashes, fname))
161            # Trick to avoid docgen_cmd.py as cmd line
162            help_txt = workflow_desc[flow_name]["help"]
163            help_txt = help_txt.replace("docgen_cmd.py", fname)
164            help_txt = help_txt.replace("usage:", format_title('usage'))
165            help_txt = help_txt.replace("positional arguments:",
166                                        format_title("positional arguments"))
167            help_txt = help_txt.replace("optional arguments:",
168                                        format_title("optional arguments"))
169            help_txt = help_txt.replace(
170                "output arguments(optional):",
171                format_title("output arguments(optional)"))
172            help_txt = help_txt.replace("References:",
173                                        format_title("References"))
174            help_txt = help_txt.rstrip()
175            fp.write(help_txt)
176
177        cmd_list.append(out_fname)
178        print("Done")
179
180    # generate index.rst
181    print("Generating index.rst")
182    with open(pjoin(outdir, "index.rst"), "w") as index:
183        index.write(".. _workflows_reference:\n\n")
184        index.write("Command Line Utilities Reference\n")
185        index.write("================================\n\n")
186        index.write(".. toctree::\n\n")
187        for cmd in cmd_list:
188            index.write("   " + cmd)
189            index.write("\n")
190    print("Done")
191