Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(109)

Unified Diff: utils/docgen/CreateDocs.py

Issue 2092022: This script will create test documentation for AutoTest suites. (Closed) Base URL: ssh://git@chromiumos-git//autotest.git
Patch Set: Made a few last changes to clean it up. Created 10 years, 7 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View side-by-side diff with in-line comments
Download patch
« no previous file with comments | « no previous file | utils/docgen/customLogo.gif » ('j') | no next file with comments »
Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
Index: utils/docgen/CreateDocs.py
diff --git a/utils/docgen/CreateDocs.py b/utils/docgen/CreateDocs.py
new file mode 100755
index 0000000000000000000000000000000000000000..e0a26e26a135404acd6345067ff39a71280cec47
--- /dev/null
+++ b/utils/docgen/CreateDocs.py
@@ -0,0 +1,708 @@
+#!/usr/bin/python
+# Copyright (c) 2010 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+""" Parse suite control files and make HTML documentation from included tests.
+
+This program will create a list of test cases found in suite files by parsing
+through each suite control file and making a list of all of the jobs called from
+it. Once it has a list of tests, it will parse the AutoTest control file for
+each test and grab the doc strings. These doc strings, along with any
+constraints in the suite control file, will be added to the original test
+script. These new scripts will be placed in a stand alone directory. Doxygen
+will then use these files for the sole purpose of producing HTML documentation
+for all of the tests. Once HTML docs are created some post processing will be
+done against the docs to change a few strings.
+
+If this script is executed without a --src argument, it will assume it is being
+executed from <ChromeOS>/src/third_party/autotest/files/utils/docgen/ directory.
+
+Classes:
+
+ DocCreator
+ This class is responsible for all processing. It requires the following:
+ - Absolute path of suite control files.
+ - Absolute path of where to place temporary files it constructs from the
+ control files and test scripts.
+ This class makes the following assumptions:
+ - Each master suite has a README.txt file with general instructions on
+ test preparation and usage.
+ - The control file for each test has doc strings with labels of:
+ - PURPOSE: one line description of why this test exists.
+ - CRITERIA: Pass/Failure conditions.
+ - DOC: additional test details.
+ ReadNode
+ This class parses a node from a control file into a key/value pair. In this
+ context, a node represents a syntactic construct of an abstract syntax tree.
+ The root of the tree is the module object (in this case a control file). If
+ suite=True, it will assume the node is from a suite control file.
+
+Doxygen should already be configured with a configuration file called:
+doxygen.conf. This file should live in the same directory with this program.
+If you haven't installed doxygen, you'll need to install this program before
+this script is executed. This program will automatically update the doxygen.conf
+file to match self.src_tests and self.html.
+
+TODO: (kdlucas@google.com) Update ReadNode class to use the replacement module
+for the compiler module, as that has been deprecated.
+"""
+
+__author__ = 'kdlucas@google.com (Kelly Lucas)'
+__version__ = '0.8.0'
+
+import compiler
+import fileinput
+import glob
+import logging
+import optparse
+import os
+import re
+import shutil
+import subprocess
+import sys
+
+
+class DocCreator(object):
+ """Process suite control files to combine docstrings and create HTML docs.
+
+ The DocCreator class is designed to parse AutoTest suite control files to
+ find all of the tests referenced, and build HTML documentation based on the
+ docstrings in those files. It will cross reference the test control file
+ and any parameters passed through the suite file, with the original test
+ case. DocCreator relies on doxygen to actually generate the HTML documents.
+
+ The workflow is as follows:
+ - Parse the suite file(s) and generate a test list.
+ - Locate the test source, and grab the docstrings from the associated
+ AutoTest control file.
+ - Combine the docstring from the control file with any parameters passed
+ in from the suite control file, with the original test case.
+ - Write a new test file with the combined docstrings to src_tests.
+ - Create HTML documentation by running doxygen against the tests stored
+ in self.src_tests.
+
+ Implements the following methods:
+ - SetLogger() - Gets a logger and sets the formatting.
+ - GetTests() - Parse suite control files, create a dictionary of tests.
+ - ParseControlFiles() - Runs through all tests and parses control files
+ - _CleanDir() - Remove any files in a direcory and create an empty one.
+ - _GetDoctString() - Parses docstrings and joins it with constraints.
+ - _CreateTest() - Add docstrings and constraints to existing test script
+ to form a new test script.
+ - CreateMainPage() - Create a mainpage.txt file based on contents of the
+ suite README file.
+ - _ConfigDoxygen - Updates doxygen.conf to match some attributes this
+ script was run with.
+ - RunDoxygen() - Executes the doxygen program.
+ - CleanDocs() - Changes some text in the HTML files to conform to our
+ naming conventions and style.
+
+ Depends upon class ReadNode.
+ """
+ def __init__(self):
+ """Parse command line arguments and set some initial variables."""
+
+ desc="""%prog will scan AutoTest suite control files to build a list of
+ test cases called in the suite, and build HTML documentation based on
+ the docstrings it finds in the tests, control files, and suite control
+ files.
+ """
+
+ self.runpath = os.path.abspath('.')
+ src = os.path.join(self.runpath, '../../../../../')
+ srcdir = os.path.abspath(src)
+
+ parser = optparse.OptionParser(description=desc,
+ prog='CreateDocs',
+ version=__version__,
+ usage='%prog')
+ parser.add_option('--debug',
+ help='Debug level [default: %default]',
+ default='debug',
+ dest='debug')
+ parser.add_option('--docversion',
+ help='Specify a version for the documentation'
+ '[default: %default]',
+ default=None,
+ dest='docversion')
+ parser.add_option('--doxy',
+ help='doxygen configuration file [default: %default]',
+ default='doxygen.conf',
+ dest='doxyconf')
+ parser.add_option('--html',
+ help='path to store html docs [default: %default]',
+ default='html',
+ dest='html')
+ parser.add_option('--latex',
+ help='path to store latex docs [default: %default]',
+ default='latex',
+ dest='latex')
+ parser.add_option('--layout',
+ help='doxygen layout file [default: %default]',
+ default='doxygenLayout.xml',
+ dest='layout')
+ parser.add_option('--log',
+ help='Logfile for program output [default: %default]',
+ default='docCreator.log',
+ dest='logfile')
+ parser.add_option('--readme',
+ help='filename of suite documentation'
+ '[default: %default]',
+ default='README.txt',
+ dest='readme')
+ parser.add_option('--src',
+ help='path to chromiumos source root'
+ ' [default: %default]',
+ default=srcdir,
+ dest='src_chromeos')
+ #TODO(kdlucas): add an all option that will parse all suites.
+ parser.add_option('--suite',
+ help='Directory name of suite [default: %default]',
+ type='choice',
+ default='suite_HWQual',
+ choices = [
+ 'suite_Factory',
+ 'suite_HWConfig',
+ 'suite_HWQual',
+ ],
+ dest='suite')
+ parser.add_option('--tests',
+ help='Absolute path of temporary test files'
+ ' [default: %default]',
+ default='testsource',
+ dest='src_tests')
+
+ self.options, self.args = parser.parse_args()
+
+
+ # Make parameters a little shorter by making the following assignments.
+ self.debug = self.options.debug
+ self.docversion = self.options.docversion
+ self.doxyconf = self.options.doxyconf
+ self.html = self.options.html
+ self.latex = self.options.latex
+ self.layout = self.options.layout
+ self.logfile = self.options.logfile
+ self.readme = self.options.readme
+ self.src_tests = self.options.src_tests
+ self.src = self.options.src_chromeos
+ self.suite = self.options.suite
+
+ autotest_path = 'third_party/autotest/files'
+ sitetests = 'client/site_tests'
+ tests = 'client/tests'
+ self.testcase = {}
+
+ self.site_dir = os.path.join(self.src, autotest_path, sitetests)
+ self.test_dir = os.path.join(self.src, autotest_path, tests)
+ self.suite_dir = os.path.join(self.site_dir, self.suite)
+
+ self.logger = self.SetLogger('docCreator')
+ self.logger.debug('Executing with debug level: %s', self.debug)
+ self.logger.debug('Writing to logfile: %s', self.logfile)
+ self.logger.debug('New test directory: %s', self.src_tests)
+ self.logger.debug('Chrome OS src directory: %s', self.src)
+ self.logger.debug('Test suite: %s', self.suite)
+
+ self.suitename = {'suite_Factory': 'Factory Testing',
+ 'suite_HWConfig': 'Hardware Configuration',
+ 'suite_HWQual': 'Hardware Qualification',
+ }
+
+ def SetLogger(self, namespace):
+ """Create a logger with some good formatting options.
+
+ Args:
+ namespace: string, name associated with this logger.
+ Returns:
+ Logger object.
+ This method assumes self.logfile and self.debug are already set.
+ This logger will write to stdout as well as a log file.
+ """
+
+ loglevel = {'debug': logging.DEBUG,
+ 'info': logging.INFO,
+ 'warning': logging.WARNING,
+ 'error': logging.ERROR,
+ 'critical': logging.CRITICAL,
+ }
+
+ logger = logging.getLogger(namespace)
+ c = logging.StreamHandler()
+ h = logging.FileHandler(os.path.join(self.runpath, self.logfile))
+ hf = logging.Formatter(
+ '%(asctime)s %(process)d %(levelname)s: %(message)s')
+ cf = logging.Formatter('%(levelname)s: %(message)s')
+ logger.addHandler(h)
+ logger.addHandler(c)
+ h.setFormatter(hf)
+ c.setFormatter(cf)
+
+ if self.debug in loglevel:
+ logger.setLevel(loglevel.get(self.debug, logging.INFO))
petkov 2010/05/26 19:35:17 Remove if / else -- you don't need them any more.
+ else:
+ logger.setLevel(logging.INFO)
+
+ return logger
+
+
+ def GetTests(self):
+ """Create dictionary of tests based on suite control file contents."""
+
+ suite_search = os.path.join(self.suite_dir, 'control.*')
+ for suitefile in glob.glob(suite_search):
+ self.logger.debug('Scanning %s for tests', suitefile)
+ try:
+ suite = compiler.parseFile(suitefile)
+ except SyntaxError, e:
+ self.logger.error('Error parsing: %s\n%s', (suitefile, e))
+ raise SystemExit
+
+ # Walk through each node found in the control file, which in our
+ # case will be a call to a test. compiler.walk() will walk through
+ # each component node, and call the appropriate function in class
+ # ReadNode. The returned key should be a string, and the name of a
+ # test. visitor.value should be any extra arguments found in the
+ # suite file that are used with that test case.
+ for n in suite.node.nodes:
+ visitor = ReadNode(suite=True)
+ compiler.walk(n, visitor)
+ if len(visitor.key) > 1:
+ self.logger.debug('Found test %s', visitor.key)
+ filtered_input = ''
+ # Lines in value should start with ' -' for bullet item.
+ if visitor.value:
+ lines = visitor.value.split('\n')
+ for line in lines:
+ if line.startswith(' -'):
+ filtered_input += line + '\n'
+ # A test could be called multiple times, so see if the key
+ # already exists, and if so append the new value.
+ if visitor.key in self.testcase:
+ s = self.testcase[visitor.key] + filtered_input
+ self.testcase[visitor.key] = s
+ else:
+ self.testcase[visitor.key] = filtered_input
+
+
+ def _CleanDir(self, directory):
+ """Ensure the directory is available and empty.
+
+ Args:
+ directory: string, path of directory
+ """
+
+ if os.path.isdir(directory):
+ try:
+ shutil.rmtree(directory)
+ except IOError, err:
+ self.logger.error('Error cleaning %s\n%s', (directory, err))
+ try:
+ os.makedirs(directory)
+ except IOError, err:
+ self.logger.error('Error creating %s\n%s', (directory, err))
+ self.logger.error('Check your permissions of %s', directory)
+ raise SystemExit
+
+
+ def ParseControlFiles(self):
+ """Get docstrings from control files and add them to new test scripts.
+
+ This method will cycle through all of the tests and attempt to find
+ their control file. If found, it will parse the docstring from the
+ control file, add this to any parameters found in the suite file, and
+ add this combined docstring to the original test. These new tests will
+ be written in the self.src_tests directory.
+ """
+ # Clean some target directories.
+ for d in [self.src_tests, self.html]:
+ self._CleanDir(d)
+
+ for test in self.testcase:
+ testdir = os.path.join(self.site_dir, test)
+ if not os.path.isdir(testdir):
+ testdir = os.path.join(self.test_dir, test)
+
+ if os.path.isdir(testdir):
+ control_file = os.path.join(testdir, 'control')
+ test_file = os.path.join(testdir, test + '.py')
+ docstring = self._GetDocString(control_file, test)
+ self._CreateTest(test_file, docstring, test)
+ else:
+ self.logger.warning('Cannot find test: %s', test)
+
+ def _GetDocString(self, control_file, test):
+ """Get the docstrings from control file and join to suite file params.
+
+ Args:
+ control_file: string, absolute path to test control file.
+ test: string, name of test.
+ Returns:
+ string: combined docstring with needed markup language for doxygen.
+ """
+
+ # Doxygen needs the @package marker.
+ package_doc = '## @package '
+ # To allow doxygen to use special commands, we must use # for comments.
+ comment = '# '
+ endlist = ' .\n'
+ control_dict = {}
+ output = []
+ temp = []
+ tempstring = ''
+ docstring = ''
+ keys = ['\\brief\n', '<H3>Pass/Fail Criteria:</H3>\n',
+ '<H3>Author</H3>\n', '<H3>Test Duration</H3>\n',
+ '<H3>Category</H3>\n', '<H3>Test Type</H3>\n',
+ '<H3>Test Class</H3>\n', '<H3>Notest</H3>\n',
+ ]
+
+ try:
+ control = compiler.parseFile(control_file)
+ except SyntaxError, e:
+ self.logger.error('Error parsing: %s\n%s', (control_file, e))
+ return None
+
+ for n in control.node.nodes:
+ visitor = ReadNode()
+ compiler.walk(n, visitor)
+ control_dict[visitor.key] = visitor.value
+
+ for k in keys:
+ if k in control_dict:
+ if len(control_dict[k]) > 1:
+ if k != test:
+ temp.append(k)
+ temp.append(control_dict[k])
+ if control_dict[k]:
+ temp.append(endlist)
+ # Add constraints and extra args after the Criteria section.
+ if 'Criteria:' in k:
+ if self.testcase[test]:
+ temp.append('<H3>Arguments:</H3>\n')
+ temp.append(self.testcase[test])
+ # '.' character at the same level as the '-' tells
+ # doxygen this is the end of the list.
+ temp.append(endlist)
+
+ output.append(package_doc + test + '\n')
+ tempstring = "".join(temp)
+ lines = tempstring.split('\n')
+ for line in lines:
+ # Doxygen requires a '#' character to add special doxygen commands.
+ comment_line = comment + line + '\n'
+ output.append(comment_line)
+
+ docstring = "".join(output)
+
+ return docstring
+
+
+ def _CreateTest(self, test_file, docstring, test):
+ """Create a new test with the combined docstrings from multiple sources.
+
+ Args:
+ test_file: string, file name of new test to write.
+ docstring: string, the docstring to add to the existing test.
+ test: string, name of the test.
+
+ This method is used to create a temporary copy of a new test, that will
+ be a combination of the original test plus the docstrings from the
+ control file, and any constraints from the suite control file.
+ """
+
+ class_def = 'class ' + test
+ pathname = os.path.join(self.src_tests, test + '.py')
+
+ # Open the test and write out new test with added docstrings
+ try:
+ f = open(test_file, 'r')
+ except IOError, err:
+ self.logger.error('Error while reading %s\n%s', (test_file, err))
+ return
+ lines = f.readlines()
+ f.close()
+
+ try:
+ f = open(pathname, 'w')
+ except IOError, err:
+ self.logger.error('Error creating %s\n%s', (pathname, err))
+ return
+
+ for line in lines:
+ if class_def in line:
+ f.write(docstring)
+ f.write('\n')
+ f.write(line)
+ f.close()
+
+ def CreateMainPage(self):
+ """Create a main page to provide content for index.html.
+
+ This method assumes a file named README.txt is located in your suite
+ directory with general instructions on setting up and using the suite.
+ If your README file is in another file, ensure you pass a --readme
+ option with the correct filename. To produce a better looking
+ landing page, use the '-' character for list items. This method assumes
+ os commands start with '$'.
+ """
+
+ # Define some strings that Doxygen uses for specific formatting.
+ cstart = '/**'
+ cend = '**/'
+ mp = '@mainpage'
+ section_begin = '@section '
+ vstart = '@verbatim '
+ vend = ' @endverbatim\n'
+
+ # Define some characters we expect to delineate sections in the README.
+ sec_char = '=========='
+ command_prompt = '$ '
+ command_cont = '\\'
+
+ command = False
+ comment = False
+ section = False
+ sec_ctr = 0
+
+ readme_file = os.path.join(self.suite_dir, self.readme)
+ mainpage_file = os.path.join(self.src_tests, 'mainpage.txt')
+
+ try:
+ f = open(readme_file, 'r')
+ except IOError, err:
+ self.logger.error('Error opening %s\n%s', (readme_file, err))
+ return
+ try:
+ fw = open(mainpage_file, 'w')
+ except IOError, err:
+ self.logger.error('Error opening %s\n%s', (mainpage_file, err))
+ return
+
+ lines = f.readlines()
+ f.close()
+
+ fw.write(cstart)
+ fw.write('\n')
+ fw.write(mp)
+ fw.write('\n')
+
+ for line in lines:
+ if sec_char in line:
+ comment = True
+ section = not section
+ elif section:
+ sec_ctr += 1
+ section_name = ' section%d ' % sec_ctr
+ fw.write(section_begin + section_name + line)
+ else:
+ # comment is used to denote when we should start recording text
+ # from the README file. Some of the initial text is not needed.
+ if comment:
+ if command_prompt in line:
+ line = line.rstrip()
+ if line[-1] == command_cont:
+ fw.write(vstart + line[:-1])
+ command = True
+ else:
+ fw.write(vstart + line + vend)
+ elif command:
+ line = line.strip()
+ if line[-1] == command_cont:
+ fw.write(line)
+ else:
+ fw.write(line + vend)
+ command = False
+ else:
+ fw.write(line)
+
+ fw.write('\n')
+ fw.write(cend)
+ fw.close()
+
+ def _ConfigDoxygen(self):
+ """Set Doxygen configuration to match our options."""
+
+ doxy_config = {
+ 'ALPHABETICAL_INDEX': 'YES',
+ 'EXTRACT_ALL': 'YES',
+ 'EXTRACT_LOCAL_METHODS': 'YES',
+ 'EXTRACT_PRIVATE': 'YES',
+ 'EXTRACT_STATIC': 'YES',
+ 'FILE_PATTERNS': '*.py *.txt',
+ 'FULL_PATH_NAMES ': 'YES',
+ 'GENERATE_TREEVIEW': 'YES',
+ 'HTML_DYNAMIC_SECTIONS': 'YES',
+ 'HTML_FOOTER': 'footer.html',
+ 'HTML_HEADER': 'header.html',
+ 'HTML_OUTPUT ': self.html,
+ 'INLINE_SOURCES': 'YES',
+ 'INPUT ': self.src_tests,
+ 'JAVADOC_AUTOBRIEF': 'YES',
+ 'LATEX_OUTPUT ': self.latex,
+ 'LAYOUT_FILE ': self.layout,
+ 'OPTIMIZE_OUTPUT_JAVA': 'YES',
+ 'PROJECT_NAME ': self.suitename[self.suite],
+ 'PROJECT_NUMBER': self.docversion,
+ 'SOURCE_BROWSER': 'YES',
+ 'STRIP_CODE_COMMENTS': 'NO',
+ 'TAB_SIZE': '4',
+ 'USE_INLINE_TREES': 'YES',
+ }
+
+ doxy_layout = {
+ 'tab type="mainpage"': 'title="%s"' %
+ self.suitename[self.suite],
+ 'tab type="namespaces"': 'title="Tests"',
+ 'tab type="namespacemembers"': 'title="Test Functions"',
+ }
+
+ for line in fileinput.input(self.doxyconf, inplace=1):
+ for k in doxy_config:
+ if line.startswith(k):
+ line = '%s = %s\n' % (k, doxy_config[k])
+ print line,
+
+ for line in fileinput.input('header.html', inplace=1):
+ if line.startswith('<H2>'):
+ line = '<H2>%s</H2>\n' % self.suitename[self.suite]
+ print line,
+
+ for line in fileinput.input(self.layout, inplace=1):
+ for k in doxy_layout:
+ if line.find(k) != -1:
+ line = line.replace('title=""', doxy_layout[k])
+ print line,
+
+
+ def RunDoxygen(self, doxyargs):
+ """Execute Doxygen on the files in the self.src_tests directory.
+
+ Args:
+ doxyargs: string, any command line args to be passed to doxygen.
+ """
+
+ doxycmd = 'doxygen %s' % doxyargs
+
+ p = subprocess.Popen(doxycmd, shell=True, stdout=subprocess.PIPE,
+ stderr=subprocess.PIPE)
+ p.wait()
+ cmdoutput = p.stdout.read()
+
+ if p.returncode:
+ self.logger.error('Error while running %s', doxycmd)
+ self.logger.error(cmdoutput)
+ else:
+ self.logger.info('%s successfully ran', doxycmd)
+
+
+ def CreateDocs(self):
+ """Configure and execute Doxygen to create HTML docuements."""
+
+ # First run doxygen with args to create default configuration files.
+ # Create layout xml file.
+ doxyargs = '-l %s' % self.layout
+ self.RunDoxygen(doxyargs)
+
+ # Create doxygen configuration file.
+ doxyargs = '-g %s' % self.doxyconf
+ self.RunDoxygen(doxyargs)
+
+ # Edit the configuration files to match our options.
+ self._ConfigDoxygen()
+
+ # Run doxygen with configuration file as argument.
+ self.RunDoxygen(self.doxyconf)
+
+
+ def CleanDocs(self):
+ """Run some post processing on the newly created docs."""
+
+ logo_image = 'customLogo.gif'
+
+ # Key = original string, value = replacement string.
+ replace = {
+ '>Package': '>Test',
+ }
+
+ docpages = os.path.join(self.html, '*.html')
+ files = glob.glob(docpages)
+ for file in files:
+ for line in fileinput.input(file, inplace=1):
+ for k in replace:
+ if line.find(k) != -1:
+ line = line.replace(k, replace[k])
+ print line,
+
+ shutil.copy(logo_image, self.html)
+
+ self.logger.info('Sanitized documentation completed.')
+
+
+class ReadNode(object):
+ """Parse a compiler node object from a control file.
+
+ Args:
+ suite: boolean, set to True if parsing nodes from a suite control file.
+ """
+
+ def __init__(self, suite=False):
+ self.key = ''
+ self.value = ''
+ self.testdef = False
+ self.suite = suite
+ self.bullet = ' - '
+
+ def visitName(self, n):
+ if n.name == 'job':
+ self.testdef = True
+
+ def visitConst(self, n):
+ if self.testdef:
+ self.key = str(n.value)
+ self.testdef = False
+ else:
+ self.value += str(n.value) + '\n'
+
+ def visitKeyword(self, n):
+ if n.name != 'constraints':
+ self.value += self.bullet + n.name + ': '
+ for item in n.expr:
+ if isinstance(item, compiler.ast.Const):
+ for i in item:
+ self.value += self.bullet + str(i) + '\n'
+ self.value += ' .\n'
+ else:
+ self.value += str(item) + '\n'
+
+
+ def visitAssName(self, n):
+ # To remove section from appearing in the documentation, set value = ''.
+ sections = {
+ 'AUTHOR': '',
+ 'CRITERIA': '<H3>Pass/Fail Criteria:</H3>\n',
+ 'DOC': '<H3>Notes</H3>\n',
+ 'NAME': '',
+ 'PURPOSE': '\\brief\n',
+ 'TIME': '<H3>Test Duration</H3>\n',
+ 'TEST_CATEGORY': '<H3>Category</H3>\n',
+ 'TEST_CLASS': '<H3>Test Class</H3>\n',
+ 'TEST_TYPE': '<H3>Test Type</H3>\n',
+ }
+
+ if not self.suite:
+ self.key = sections.get(n.name, n.name)
+
+
+def main():
+ doc = DocCreator()
+ doc.GetTests()
+ doc.ParseControlFiles()
+ doc.CreateMainPage()
+ doc.CreateDocs()
+ doc.CleanDocs()
+
+
+if __name__ == '__main__':
+ main()
« no previous file with comments | « no previous file | utils/docgen/customLogo.gif » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698