Logo AND Algorithmique Numérique Distribuée

Public GIT Repository
b0de46f567676267b1b34b4b6a6de34ee1757e27
[simgrid.git] / docs / source / _ext / javasphinx / apidoc.py
1 #
2 # Copyright 2012-2015 Bronto Software, Inc. and contributors
3 #
4 # Licensed under the Apache License, Version 2.0 (the "License");
5 # you may not use this file except in compliance with the License.
6 # You may obtain a copy of the License at
7 #
8 #     http://www.apache.org/licenses/LICENSE-2.0
9 #
10 # Unless required by applicable law or agreed to in writing, software
11 # distributed under the License is distributed on an "AS IS" BASIS,
12 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 # See the License for the specific language governing permissions and
14 # limitations under the License.
15 #
16
17 from __future__ import print_function, unicode_literals
18
19 try:
20    import cPickle as pickle
21 except:
22    import pickle
23
24 import hashlib
25 import logging
26 import sys
27 import os
28 import os.path
29
30 from optparse import OptionParser
31
32 import javalang
33
34 import javasphinx.compiler as compiler
35 import javasphinx.util as util
36
37 def encode_output(s):
38    if isinstance(s, str):
39       return s
40    else:
41       return s.encode('utf-8')
42
43 def find_source_files(input_path, excludes):
44     """ Get a list of filenames for all Java source files within the given
45     directory.
46
47     """
48
49     java_files = []
50
51     input_path = os.path.normpath(os.path.abspath(input_path))
52
53     for dirpath, dirnames, filenames in os.walk(input_path):
54         if is_excluded(dirpath, excludes):
55             del dirnames[:]
56             continue
57
58         for filename in filenames:
59             if filename.endswith(".java"):
60                 java_files.append(os.path.join(dirpath, filename))
61
62     return java_files
63
64 def write_toc(packages, opts):
65     doc = util.Document()
66     doc.add_heading(opts.toc_title, '=')
67
68     toc = util.Directive('toctree')
69     toc.add_option('maxdepth', '2')
70     doc.add_object(toc)
71
72     for package in sorted(packages.keys()):
73         toc.add_content("%s/package-index\n" % package.replace('.', '/'))
74
75     filename = 'packages.' + opts.suffix
76     fullpath = os.path.join(opts.destdir, filename)
77
78     if os.path.exists(fullpath) and not (opts.force or opts.update):
79         sys.stderr.write(fullpath + ' already exists. Use -f to overwrite.\n')
80         sys.exit(1)
81
82     f = open(fullpath, 'w')
83     f.write(encode_output(doc.build()))
84     f.close()
85
86 def write_documents(packages, documents, sources, opts):
87     package_contents = dict()
88
89     # Write individual documents
90     for fullname, (package, name, document) in documents.items():
91         if is_package_info_doc(name):
92             continue
93
94         package_path = package.replace('.', os.sep)
95         filebasename = name.replace('.', '-')
96         filename = filebasename + '.' + opts.suffix
97         dirpath = os.path.join(opts.destdir, package_path)
98         fullpath = os.path.join(dirpath, filename)
99
100         if not os.path.exists(dirpath):
101             os.makedirs(dirpath)
102         elif os.path.exists(fullpath) and not (opts.force or opts.update):
103             sys.stderr.write(fullpath + ' already exists. Use -f to overwrite.\n')
104             sys.exit(1)
105
106         # Add to package indexes
107         package_contents.setdefault(package, list()).append(filebasename)
108
109         if opts.update and os.path.exists(fullpath):
110             # If the destination file is newer than the source file than skip
111             # writing it out
112             source_mod_time = os.stat(sources[fullname]).st_mtime
113             dest_mod_time = os.stat(fullpath).st_mtime
114
115             if source_mod_time < dest_mod_time:
116                 continue
117
118         f = open(fullpath, 'w')
119         f.write(encode_output(document))
120         f.close()
121
122     # Write package-index for each package
123     for package, classes in package_contents.items():
124         doc = util.Document()
125         doc.add_heading(package, '=')
126
127         #Adds the package documentation (if any)
128         if packages[package] != '':
129             documentation = packages[package]
130             doc.add_line("\n%s" % documentation)
131
132         doc.add_object(util.Directive('java:package', package))
133
134         toc = util.Directive('toctree')
135         toc.add_option('maxdepth', '1')
136
137         classes.sort()
138         for filebasename in classes:
139             toc.add_content(filebasename + '\n')
140         doc.add_object(toc)
141
142         package_path = package.replace('.', os.sep)
143         filename = 'package-index.' + opts.suffix
144         dirpath = os.path.join(opts.destdir, package_path)
145         fullpath = os.path.join(dirpath, filename)
146
147         if not os.path.exists(dirpath):
148             os.makedirs(dirpath)
149         elif os.path.exists(fullpath) and not (opts.force or opts.update):
150             sys.stderr.write(fullpath + ' already exists. Use -f to overwrite.\n')
151             sys.exit(1)
152
153         f = open(fullpath, 'w')
154         f.write(encode_output(doc.build()))
155         f.close()
156
157 def get_newer(a, b):
158     if not os.path.exists(a):
159         return b
160
161     if not os.path.exists(b):
162         return a
163
164     a_mtime = int(os.stat(a).st_mtime)
165     b_mtime = int(os.stat(b).st_mtime)
166
167     if a_mtime < b_mtime:
168         return b
169
170     return a
171
172 def format_syntax_error(e):
173     rest = ""
174     if e.at.position:
175         value = e.at.value
176         pos = e.at.position
177         rest = ' at %s line %d, character %d' % (value, pos[0], pos[1])
178     return e.description + rest
179
180 def generate_from_source_file(doc_compiler, source_file, cache_dir):
181     if cache_dir:
182         fingerprint = hashlib.md5(source_file.encode()).hexdigest()
183         cache_file = os.path.join(cache_dir, 'parsed-' + fingerprint + '.p')
184
185         if get_newer(source_file, cache_file) == cache_file:
186             return pickle.load(open(cache_file, 'rb'))
187     else:
188         cache_file = None
189
190     f = open(source_file)
191     source = f.read()
192     f.close()
193
194     try:
195         ast = javalang.parse.parse(source)
196     except javalang.parser.JavaSyntaxError as e:
197         util.error('Syntax error in %s: %s', source_file, format_syntax_error(e))
198     except Exception:
199         util.unexpected('Unexpected exception while parsing %s', source_file)
200
201     documents = {}
202     try:
203         if source_file.endswith("package-info.java"):
204             if ast.package is not None:
205                 documentation = doc_compiler.compile_docblock(ast.package)
206                 documents[ast.package.name] = (ast.package.name, 'package-info', documentation)
207         else:
208             documents = doc_compiler.compile(ast)
209     except Exception:
210         util.unexpected('Unexpected exception while compiling %s', source_file)
211
212     if cache_file:
213         dump_file = open(cache_file, 'wb')
214         pickle.dump(documents, dump_file)
215         dump_file.close()
216
217     return documents
218
219 def generate_documents(source_files, cache_dir, verbose, member_headers, parser):
220     documents = {}
221     sources = {}
222     doc_compiler = compiler.JavadocRestCompiler(None, member_headers, parser)
223
224     for source_file in source_files:
225         if verbose:
226             print('Processing', source_file)
227
228         this_file_documents = generate_from_source_file(doc_compiler, source_file, cache_dir)
229         for fullname in this_file_documents:
230             sources[fullname] = source_file
231
232         documents.update(this_file_documents)
233
234     #Existing packages dict, where each key is a package name
235     #and each value is the package documentation (if any)
236     packages = {}
237
238     #Gets the name of the package where the document was declared
239     #and adds it to the packages dict with no documentation.
240     #Package documentation, if any, will be collected from package-info.java files.
241     for package, name, _ in documents.values():
242         packages[package] = ""
243
244     #Gets packages documentation from package-info.java documents (if any).
245     for package, name, content in documents.values():
246         if is_package_info_doc(name):
247             packages[package] = content
248
249     return packages, documents, sources
250
251 def normalize_excludes(rootpath, excludes):
252     f_excludes = []
253     for exclude in excludes:
254         if not os.path.isabs(exclude) and not exclude.startswith(rootpath):
255             exclude = os.path.join(rootpath, exclude)
256         f_excludes.append(os.path.normpath(exclude) + os.path.sep)
257     return f_excludes
258
259 def is_excluded(root, excludes):
260     sep = os.path.sep
261     if not root.endswith(sep):
262         root += sep
263     for exclude in excludes:
264         if root.startswith(exclude):
265             return True
266     return False
267
268 def is_package_info_doc(document_name):
269     ''' Checks if the name of a document represents a package-info.java file. '''
270     return document_name == 'package-info'
271
272
273 def main(argv=sys.argv):
274     logging.basicConfig(level=logging.WARN)
275
276     parser = OptionParser(
277         usage="""\
278 usage: %prog [options] -o <output_path> <input_path> [exclude_paths, ...]
279
280 Look recursively in <input_path> for Java sources files and create reST files
281 for all non-private classes, organized by package under <output_path>. A package
282 index (package-index.<ext>) will be created for each package, and a top level
283 table of contents will be generated named packages.<ext>.
284
285 Paths matching any of the given exclude_paths (interpreted as regular
286 expressions) will be skipped.
287
288 Note: By default this script will not overwrite already created files.""")
289
290     parser.add_option('-o', '--output-dir', action='store', dest='destdir',
291                       help='Directory to place all output', default='')
292     parser.add_option('-f', '--force', action='store_true', dest='force',
293                       help='Overwrite all files')
294     parser.add_option('-c', '--cache-dir', action='store', dest='cache_dir',
295                       help='Directory to stored cachable output')
296     parser.add_option('-u', '--update', action='store_true', dest='update',
297                       help='Overwrite new and changed files', default=False)
298     parser.add_option('-T', '--no-toc', action='store_true', dest='notoc',
299                       help='Don\'t create a table of contents file')
300     parser.add_option('-t', '--title', dest='toc_title', default='Javadoc',
301                       help='Title to use on table of contents')
302     parser.add_option('--no-member-headers', action='store_false', default=True, dest='member_headers',
303                       help='Don\'t generate headers for class members')
304     parser.add_option('-s', '--suffix', action='store', dest='suffix',
305                       help='file suffix (default: rst)', default='rst')
306     parser.add_option('-I', '--include', action='append', dest='includes',
307                       help='Additional input paths to scan', default=[])
308     parser.add_option('-p', '--parser', dest='parser_lib', default='lxml',
309                       help='Beautiful Soup---html parser library option.')
310     parser.add_option('-v', '--verbose', action='store_true', dest='verbose',
311                       help='verbose output')
312
313     (opts, args) = parser.parse_args(argv[1:])
314
315     if not args:
316         parser.error('A source path is required.')
317
318     rootpath, excludes = args[0], args[1:]
319
320     input_paths = opts.includes
321     input_paths.append(rootpath)
322
323     if not opts.destdir:
324         parser.error('An output directory is required.')
325
326     if opts.suffix.startswith('.'):
327         opts.suffix = opts.suffix[1:]
328
329     for input_path in input_paths:
330         if not os.path.isdir(input_path):
331             sys.stderr.write('%s is not a directory.\n' % (input_path,))
332             sys.exit(1)
333
334     if not os.path.isdir(opts.destdir):
335         os.makedirs(opts.destdir)
336
337     if opts.cache_dir and not os.path.isdir(opts.cache_dir):
338         os.makedirs(opts.cache_dir)
339
340     excludes = normalize_excludes(rootpath, excludes)
341     source_files = []
342
343     for input_path in input_paths:
344         source_files.extend(find_source_files(input_path, excludes))
345
346     packages, documents, sources = generate_documents(source_files, opts.cache_dir, opts.verbose,
347                                                       opts.member_headers, opts.parser_lib)
348
349     write_documents(packages, documents, sources, opts)
350
351     if not opts.notoc:
352         write_toc(packages, opts)