1
2 """Contains basic classes and functionaliy"""
3 __docformat__ = "restructuredtext"
4 import os
5 import sys
6 import optparse
7 import subprocess
8 import shutil
9
10 import mrv
11 import mrv.doc
12
13 import mrv.test.cmd as cmd
14
15 from mrv.path import (make_path, Path)
16
17 ospd = os.path.dirname
18
19
20 __all__ = [ "DocGenerator" ]
24 """Encapsulates all functionality required to create sphinx/epydoc documentaiton"""
25
26
27 forbidden_dirs = ['test', 'ext', 'doc', '.']
28
29
30 source_dir = make_path('source')
31 source_dl_dir = source_dir / 'download'
32
33 build_dir = make_path('build')
34
35 html_dir = build_dir / 'html'
36 downloads_dir = html_dir / '_downloads'
37
38
39 epydoc_show_source = 'yes'
40 epydoc_modules = """modules: unittest
41 modules: ../mrv,../mrv/ext/networkx/networkx,../mrv/ext/pyparsing/src,../mrv/ext/pydot"""
42
43 epydoc_exclude = "mrv.test,mrv.cmd.ipythonstartup"
44
45
46
47
48 package_name = None
49
50
51
52
53 pinfo = None
54 rootmodule = None
55 epydoc_cfg = """[epydoc]
56 name: %s
57 url: %s
58
59 sourcecode: %s
60 %s
61
62 exclude: %s
63 output: html"""
64
65
66
67 - def __init__(self, sphinx=True, sphinx_autogen=True, coverage=True, epydoc=True, base_dir='.', *args):
68 """Initialize the instance
69
70 :param sphinx: If True, sphinx documentation will be produced
71 :param coverage: If True, the coverage report will be generated
72 :param epydoc: If True, epydoc documentation will be generated"""
73 if self.pinfo is None:
74 self._retrieve_project_info(base_dir)
75
76
77 self._sphinx = sphinx
78 self._sphinx_autogen = sphinx_autogen
79 self._coverage = coverage
80 self._epydoc = epydoc
81
82 self._base_dir = make_path(base_dir)
83
84
85
86 if self._base_dir.abspath().basename() != 'doc':
87 raise EnvironmentError("Basedirectory needs to be the 'doc' directory, not %s" % self._base_dir.abspath())
88
89
90 self._project_dir = make_path(self._base_dir / "..")
91
92 @classmethod
107
108
109
110
111 @classmethod
113 """Remove the version info file if it exists"""
114 try:
115 os.remove(cls.version_file_name(idstring, basedir))
116 except OSError:
117 pass
118
119
120 @classmethod
122 """:return: filename at which to write the version file with the given id"""
123 return make_path(os.path.join(basedir, "%s.version_info" % idstring))
124
125 @classmethod
127 """Writes a version file containing the rootmodule's version info.
128 This allows to verify that the version of the individual parts, like
129 epydoc and sphinx are still matching"""
130 version_string = "version_info = (%i, %i, %i, '%s', %i)" % cls.pinfo.version
131 open(cls.version_file_name(idstring, basedir), 'wb').write(version_string)
132
133 @classmethod
135 """Checks whether the current version info matches with the stored version info
136 as retrieved from idstring.
137 If there is no such info or if the version matches exactly, do nothing.
138 Otherwise raise an environment error to tell the user to rebuild the
139 respective part of the documentation"""
140 vlocals = dict()
141 vfile = cls.version_file_name(idstring, basedir)
142 if not os.path.isfile(vfile):
143 return
144
145 execfile(vfile, vlocals)
146 vinfo = vlocals['version_info']
147 if vinfo != cls.pinfo.version:
148 msg = "Documentation target named '%s' at version %s requires '%s' ( last built at %s ) to be rebuild" % (opid, str(cls.pinfo.version), idstring, str(vinfo))
149 raise EnvironmentError(msg)
150
151
152 @classmethod
154 """:return: OptionParser instance suitable to parse commandline arguments
155 with which to initialize our instance"""
156 usage = """%prog [options]
157
158 Make documentation or remove the generated files."""
159 parser = optparse.OptionParser(usage=usage)
160
161 hlp = "Specify the name of the package to import, defaults to 'mrv'"
162 parser.add_option('-p', '--package', dest='package_name', help=hlp)
163
164 hlp = """Specifies to build sphinx documentation"""
165 parser.add_option('-s', '--sphinx', dest='sphinx', type='int',default=1,
166 help=hlp, metavar='STATE')
167
168 hlp = """If specified, sphinx API docuementation will be generated"""
169 parser.add_option('-a', '--sphinx-autogen', dest='sphinx_autogen', type='int', default=1,
170 help=hlp, metavar='STATE')
171
172 hlp = """Specifies epydoc documentation"""
173 parser.add_option('-e', '--epydoc', dest='epydoc', type='int', default=1,
174 help=hlp, metavar='STATE')
175
176 hlp = """Specifies a coverage report. It will be referenced from within the
177 sphinx documentation"""
178 parser.add_option('-c', '--coverage', dest='coverage', type='int', default=1,
179 help=hlp, metavar='STATE')
180
181 return parser
182
183 @classmethod
185 """
186 :return: path which includes our package - if it would be in the sys.path,
187 we should be able to import it
188 :param basedir: we expect to be in the root/doc path of the project - if this is not
189 the case, the basedir can be adjusted accordingly to 'virtually' chdir into the
190 doc directory"""
191 return ospd(os.path.realpath(os.path.abspath(basedir)))
192
193 @classmethod
195 """Produce the actual docs using this type"""
196 p = cls.parser()
197
198 hlp = """If specified, previously generated files will be removed. Works in conjunction
199 with the other flags, which default to True, hence %prog --clean will remove all
200 generated files by default"""
201 p.add_option('--clean', dest='clean', action='store_true', default=False, help=hlp)
202
203 options, args = p.parse_args(args)
204 clean = options.clean
205 del(options.clean)
206
207
208 cls.package_name = options.package_name or cls.package_name
209
210 default_package = 'mrv'
211 if cls.package_name is None and os.path.isdir(os.path.join(cls.root_dir(), default_package)):
212 cls.package_name = default_package
213
214
215 if cls.package_name is None:
216 p.error("Please specify the --package that should be imported")
217
218 del(options.package_name)
219
220 dgen = cls(*args, **options.__dict__)
221 if clean:
222 dgen.clean()
223 else:
224 dgen.generate()
225
226
243
244
298
299
300
301
302
303
304
306 """:return: Path containing all documentation sources and output files"""
307 return self._base_dir
308
310 """Set the base directory to the given value
311 :return: self"""
312 self._base_dir = Path(base_dir)
313 return self
314
316 """:return: Path to index rst file"""
317 return self._base_dir / self.source_dir / "index.rst"
318
320 """:return: Path to the build downloads directory"""
321 return self._base_dir / self.downloads_dir
322
324 """:return: Path to the source downloads directory"""
325 return self._base_dir / self.source_dl_dir
326
328 """:return: Path to coverage related downloads"""
329 return self.source_downloads_dir() / 'coverage'
330
332 """:return: Path to directory to which epydoc will write its output"""
333 return self.html_output_dir() / 'generated' / 'api'
334
336 """:return: html directory to receive all output"""
337 return self._base_dir / self.html_dir
338
340 """:return: directory to which sphinx-autogen will write its output to"""
341 return self._base_dir / self.source_dir / 'generated'
342
344 """:return: Path to doctrees directory to which sphinx writes some files"""
345 return self._base_dir / self.build_dir / 'doctrees'
346
351
356
357
358
359
369
370
372 """Wrapper of subprocess.call which assumes that we call a python script.
373 On windows, the python interpreter needs to be called directly
374 :raise EnvironmentError: if the called had a non-0 return value"""
375 if sys.platform.startswith('win'):
376 args[0].insert(0, "python")
377
378 cmd = ' '.join(str(i) for i in args[0])
379 print cmd
380 rval = subprocess.call(*args, **kwargs)
381
382 if rval:
383 raise EnvironmentError("Call to %s failed with status %i" % (args[0][0], rval))
384
385
386
387
388
389 @classmethod
391 """Store the project information of the actual project in our class members
392 for later use
393 :note: must be called exactly once"""
394 if cls.pinfo is not None:
395 return cls.pinfo
396
397
398 if cls.package_name is None:
399 raise ValueError("Package name needs to be set, but was None")
400
401
402
403
404
405
406 rootpath = cls.root_dir(base_dir)
407 sys.path.append(rootpath)
408
409 try:
410 cls.rootmodule = __import__(cls.package_name)
411 except ImportError:
412 raise EnvironmentError("Root package %s could not be imported" % cls.package_name)
413
414
415 pinfo_package = "%s.info" % cls.package_name
416 try:
417 cls.pinfo = __import__(pinfo_package, fromlist=[''])
418 except ImportError:
419 raise EnvironmentError("Project information module %r could not be imported:" % pinfo_package)
420
421
422
424 """Generate the index.rst file according to the modules and packages we
425 actually have"""
426 import mrv
427
428 indexpath = self.index_rst_path()
429 index_header = indexpath+'.header'
430 if not index_header.isfile():
431 return
432
433
434 ifp = open(indexpath, 'wb')
435
436 ifp.write(index_header.bytes())
437
438
439 if self._sphinx_autogen:
440 basepath = self._base_dir / ".." / self.pinfo.root_package
441 rootmodule = basepath.abspath().basename()
442 for root, dirs, files in os.walk(basepath):
443 remove_dirs = list()
444 for dirname in dirs:
445 if dirname in self.forbidden_dirs:
446 remove_dirs.append(dirname)
447
448
449
450 for dirname in remove_dirs:
451 del(dirs[dirs.index(dirname)])
452
453
454 for fname in files:
455 if not fname.endswith('.py') or fname.startswith('_'):
456 continue
457 filepath = os.path.join(root, fname)
458
459
460 modulepath = "%s.%s" % (rootmodule, filepath[len(basepath)+1:-3].replace(os.path.sep, '.'))
461 ifp.write("\t%s\n" % modulepath)
462
463
464
465
466
467 ifp.write((indexpath+'.footer').bytes())
468 ifp.close()
469
471 """Generate a coverage report and make it available as download"""
472 tmrvpath = self.tmrv_bin_path()
473
474
475
476
477 prevcwd = os.getcwd()
478 os.chdir(self._project_dir)
479
480 try:
481 rval = self._call_python_script([tmrvpath, str(self._mrv_maya_version()),
482 "%s=%s" % (cmd.tmrv_coverage_flag, self.pinfo.root_package)])
483 finally:
484 os.chdir(prevcwd)
485
486
487 if rval:
488 raise SystemError("tmrv reported failure")
489
490
491 bdd = self.build_downloads_dir()
492 csdd = self.source_downloads_coverage_dir()
493 for dir in (bdd, csdd):
494 if not dir.isdir():
495 dir.makedirs()
496
497
498
499
500
501
502 coverage_dir = make_path(self._project_dir / cmd.tmrv_coverage_dir)
503 cindex = coverage_dir / 'index.html'
504 shutil.copy(cindex, csdd)
505
506
507 for html in coverage_dir.files():
508 shutil.copy(html, bdd)
509
510
511 self.write_version('coverage')
512
513
515 """Instruct sphinx to generate the autogen rst files"""
516
517 mrvpath = self.mrv_bin_path()
518
519
520
521 code = "import mrv; import sphinx.ext.autosummary.generate as sas; sas.main()"
522 agp = self.autogen_output_dir()
523
524
525 if agp.isdir():
526 shutil.rmtree(agp)
527 agp.makedirs()
528
529
530
531
532 os.environ['MRV_INFO_DIR'] = os.path.dirname(self.pinfo.__file__)
533 args = [mrvpath, str(self._mrv_maya_version()), '-c', code,
534 '-o', agp,
535 self.index_rst_path()]
536
537 self._call_python_script(args)
538
539
540
541 if not agp.isdir():
542 print >> sys.stderr, "WARNING: No autogenerated rst files written to %s" % agp.abspath()
543 return
544
545
546
547
548
549
550 for rstfile in agp.files("*.rst"):
551
552 lines = rstfile.lines()
553 modulename = lines[0][6:-2]
554 lines.insert(2, ":api:`%s`\n" % modulename)
555
556
557 i = 0
558 l = len(lines)
559 while i < l:
560 line = lines[i]
561 if line.startswith('.. autoclass'):
562 classname = line[line.rfind(' ')+1:-1]
563 l += 1
564 lines.insert(i, ':api:`%s.%s`\n\n' % (modulename, classname))
565 i += 1
566
567 i += 1
568
569
570 rstfile.write_lines(lines)
571
572
574 """:return: list of arguments to be used when calling sphinx from the commandline
575 :note: directories of all kinds will be handled by the caller"""
576
577 return ['-c', 'import sys, mrv, sphinx.cmdline; sphinx.cmdline.main(sys.argv)',
578 '-b', 'html',
579 '-D', 'latex_paper_size=a4',
580 '-D', 'latex_paper_size=letter',
581 '-D', 'project=%s' % self.pinfo.project_name,
582 '-D', 'copyright=%s' % self.pinfo.author,
583 '-D', 'version=%s' % "%i.%i" % self.pinfo.version[:2],
584 '-D', 'release=%s' % "%i.%i.%i-%s" % self.pinfo.version[:4]]
585
607
609 """Generate epydoc documentation"""
610 try:
611 import epydoc
612 except ImportError:
613 raise ImportError("Epydoc could not be imported, please make sure it is available in your PYTHONPATH")
614
615
616 self._apply_epydoc_config()
617
618
619
620 epytarget = self.epydoc_target_dir()
621 if not epytarget.isdir():
622 epytarget.makedirs()
623
624
625
626 def visit_paragraph(this, node):
627 """Epydoc patch - will be applied on demand"""
628 if this.summary is not None:
629
630 this.other_docs = True
631 raise docutils.nodes.NodeFound('Found summary')
632
633 summary_pieces = []
634
635
636 for child in node:
637 if isinstance(child, docutils.nodes.Text):
638 m = this._SUMMARY_RE.match(child)
639 if m:
640 summary_pieces.append(docutils.nodes.Text(m.group(1)))
641 other = child[m.end():]
642 if other and not other.isspace():
643 this.other_docs = True
644 break
645 summary_pieces.append(child)
646
647 summary_doc = this.document.copy()
648 summary_para = node.copy()
649 summary_doc[:] = [summary_para]
650 summary_para[:] = summary_pieces
651 this.summary = ParsedRstDocstring(summary_doc)
652
653
654
655 epydoc_cfg_file = "epydoc.cfg"
656 open(epydoc_cfg_file, 'wb').write(self.epydoc_cfg)
657
658 args = ['epydoc', '-q', '-q', '--debug', '--config', epydoc_cfg_file, '-o', str(epytarget)]
659
660 print "Launching in-process epydoc: ", " ".join(args)
661 origargs = sys.argv[:]
662 del(sys.argv[:])
663 sys.argv.extend(args)
664 try:
665 try:
666
667 import docutils.nodes
668 import epydoc.markup.restructuredtext
669 from epydoc.markup.restructuredtext import ParsedRstDocstring
670 epydoc.markup.restructuredtext._SummaryExtractor.visit_paragraph = visit_paragraph
671
672 import epydoc.cli
673 epydoc.cli.cli()
674 except:
675 import pdb
676 print >> sys.stderr, "Epydoc encountered an exception - use pdb to figure out which code it couldn't handle"
677 pdb.post_mortem(sys.exc_info()[-1])
678 raise
679
680 finally:
681 os.remove(epydoc_cfg_file)
682 del(sys.argv[:])
683 sys.argv.extend(origargs)
684
685
686 self.write_version('epydoc')
687
688
689