| # ***************************************************************************** |
| # * Copyright (c) 2016 Wind River Systems, Inc. and others. |
| # * All rights reserved. This program and the accompanying materials |
| # * are made available under the terms of the Eclipse Public License 2.0 |
| # * which accompanies this distribution, and is available at |
| # * https://www.eclipse.org/legal/epl-2.0/ |
| # * |
| # * Contributors: |
| # * Wind River Systems - initial implementation |
| # ***************************************************************************** |
| |
| """TCF Python documentation tool. |
| |
| This tool is based on `Sphinx <http://www.sphinx-doc.org>`_, and generates |
| documentation for the reStructuredText files located in the ``rst`` directory. |
| |
| Usage |
| ===== |
| ``python docs.py [-h] [-outdir OUTDIR] [-sphinxdir SPHINXDIR] [-theme THEME] \ |
| {generate,clean,cleanall}`` |
| |
| Command line options |
| ==================== |
| |
| +----------------+----------------------------------------------------+ |
| | ``-h``, | Show this help message and exit. | |
| | ``--help`` | | |
| +----------------+----------------------------------------------------+ |
| | ``-outdir`` | Directory to generate documentation into. | |
| +----------------+----------------------------------------------------+ |
| | ``-sphinxdir`` | Directory to install sphinx into *(not for Windows | |
| | | hosts)*. | |
| +----------------+----------------------------------------------------+ |
| | ``-theme`` | The sphinx theme used for generated documentation. | |
| +----------------+----------------------------------------------------+ |
| |
| Command line arguments |
| ====================== |
| |
| ``command`` |
| ----------- |
| The ``command`` value may take any of the following values: |
| |
| - ``generate`` : Generates the html documentation using Sphinx. |
| - ``clean`` : Removes the generated documentation from output directory. |
| - ``cleanall`` : Removes the generated documentation from output directory, and |
| the Sphinx installation. |
| """ |
| |
| import argparse |
| import glob |
| import os |
| import shutil |
| import subprocess |
| import sys |
| import tarfile |
| import time |
| import zipfile |
| |
| try: |
| # Python 3 |
| import urllib.request as URLLIB # @UnresolvedImport @UnusedImport |
| except: |
| import urllib as URLLIB # @UnresolvedImport @UnusedImport @Reimport |
| |
| |
| # A bit of python 2/3 compat. The ../tcf/compat.py cannot be used as this |
| # is a different package. Full path should be used. |
| |
| ispython3 = sys.version_info[0] == 3 |
| |
| if ispython3: |
| strings = (str,) |
| else: |
| strings = (basestring, str) # @UndefinedVariable |
| |
| |
| def bytes2str(data): |
| """Converts bytes to a string. |
| |
| The integers of *data* are returned into a single string. This is |
| mainly used to transform a |bytearray| into a string the same way for |
| python 2 or 3. |
| |
| :param data: An iterable of integers (value smaller than 256 - aka |
| unsigned char values). |
| |
| :returns: An |str| representing *data* converted to characters. |
| """ |
| if data and isinstance(data[0], int): |
| return ''.join(chr(b) for b in data) |
| elif data: |
| return str(data) |
| return '' |
| |
| |
| def execute(command, path=None, msg=None, verbose=False): |
| """Execute given command in given path. |
| |
| :param command: The list of arguments of the command to execute. |
| :type command: |list| |
| :param path: The path to execute command at. If **None**, current path is |
| used. |
| :type path: |str| or **None** |
| :param msg: The message to print out for the command execution. If |
| **None**, the actual command is printed out. |
| :type msg: |str| or **None** |
| :param verbose: States if command output should be printed out. |
| :type verbose: |bool| |
| """ |
| if path is None: |
| path = os.getcwd() |
| if msg is None: |
| msg = ' '.join(command) |
| |
| msg += ' ... ' |
| |
| sys.stdout.write(msg) |
| sys.stdout.flush() |
| if verbose: |
| sys.stdout.write('\n') |
| prc = subprocess.Popen(command, cwd=path) |
| else: |
| prc = subprocess.Popen(command, cwd=path, stdout=subprocess.PIPE, |
| stderr=subprocess.STDOUT) |
| _out, err = prc.communicate() |
| if err: |
| sys.stderr.write('fail\n') |
| sys.stderr.write(str(err) + '\n') |
| raise Exception(err) |
| else: |
| sys.stdout.write('done\n') |
| |
| |
| def fullpath(path): |
| """Get the fullpath of given *path*. |
| |
| If path is a tuple or a list, elements of *path* are first joined with OS |
| separator to create the *path* string. |
| |
| If *path* is a string, it is user expanded and variables expanded. |
| |
| :param path: The path to get full path for. |
| :type path: |str| or |tuple| or |list| |
| |
| :returns: An |str| representing the full path of *path*. |
| """ |
| if path is None: |
| return path |
| |
| if isinstance(path, strings): |
| pathstr = path |
| elif isinstance(path, (list, tuple)): |
| pathstr = os.path.join(*path) |
| else: |
| raise Exception('Unsupported file path "' + str(path) + |
| '": invalid type ' + str(type(path))) |
| res = os.path.expandvars(pathstr) |
| res = os.path.expanduser(res) |
| if not os.path.isabs(res): |
| res = os.path.abspath(res) |
| res = os.path.realpath(res) |
| res = os.path.normpath(res) |
| |
| return res |
| |
| |
| def wget(url, destdir): |
| """Download a file from an URL to a directory. |
| |
| If *directroy* does not exist, it is created. |
| |
| :param url: The URL to download file from. |
| :type url: |str| |
| :param destdir: The directory to download file to. |
| :type destdir: |str| |
| """ |
| if not os.path.exists(destdir): |
| os.makedirs(destdir) |
| |
| destfile = fullpath((destdir, os.path.basename(url))) |
| |
| # Remove the destination file if needed |
| |
| if os.path.exists(destfile): |
| os.remove(destfile) |
| |
| # Determine the destination file name |
| |
| ix = 1 |
| destbase = destfile |
| |
| while os.path.exists(destfile): |
| destfile = destbase + '.' + str(ix) |
| ix += 1 |
| |
| URLLIB.urlretrieve(url, os.path.join(destdir, destfile)) |
| |
| filesize = os.path.getsize(destfile) |
| |
| msg = time.strftime('%Y-%m-%d %H:%M:%S', time.localtime()) + ' URL:' + \ |
| url + ' [' + str(filesize) + '/' + str(filesize) + '] -> "' + \ |
| destfile + '" [1]' |
| |
| sys.stdout.write(msg + '\n') |
| |
| return destfile |
| |
| |
| class Sphinx(object): |
| |
| BUILDER_HTML = 'html' |
| |
| THEME_AGOGO = 'agogo' |
| THEME_BASIC = 'basic' |
| THEME_BIZSTYLE = 'bizstyle' |
| THEME_CLASSIC = 'classic' |
| THEME_DEFAULT = 'default' |
| THEME_EPUB = 'epub' |
| THEME_HAIKU = 'haiku' |
| THEME_NATURE = 'nature' |
| THEME_PYRAMID = 'pyramid' |
| THEME_SCROLLS = 'scrolls' |
| THEME_SPHINXDOC = 'sphinxdoc' |
| THEME_TRADITIONAL = 'traditional' |
| |
| THEMES = (THEME_AGOGO, THEME_BASIC, THEME_BIZSTYLE, THEME_CLASSIC, |
| THEME_DEFAULT, THEME_EPUB, THEME_HAIKU, THEME_NATURE, |
| THEME_PYRAMID, THEME_SCROLLS, THEME_SPHINXDOC, THEME_TRADITIONAL) |
| |
| VERSION = '1.4.1' |
| |
| """An object representing a Sphinx install. |
| |
| :param path: The path to install Sphinx packages in. If **None**, the |
| current python interpreter directory is used. |
| :type path: |str| or **None** |
| """ |
| def __init__(self, path=None): |
| |
| if path: |
| self._path = path |
| else: |
| self._path = fullpath(((sys.executable), '..', '..')) |
| |
| self._bin = None |
| self._builder = self.BUILDER_HTML |
| self._cachedir = None |
| self._lib = None |
| self._name = 'Sphinx-' + self.VERSION |
| self._outdir = None |
| self._rstdir = fullpath(('.', 'rst')) |
| if ispython3: |
| self._theme = self.THEME_PYRAMID |
| else: |
| self._theme = self.THEME_DEFAULT |
| self._url_base = 'https://pypi.python.org/packages/source/S/Sphinx' |
| self._url = self._url_base + '/' + self._name + '.tar.gz' |
| self._egg = self._name + '-py' + str(sys.version_info[0]) + '.' + \ |
| str(sys.version_info[1]) + '.egg' |
| self._dependency = self.lib + '/site-packages/' + self._egg |
| self._version = self.VERSION |
| |
| @property |
| def bin(self): |
| """The binary directory for the current python interpreter.""" |
| if self._bin is None: |
| if sys.platform in ('win32', 'cygwin'): |
| self._bin = fullpath((sys.executable, '..')) |
| else: |
| self._bin = fullpath((self.path, 'bin')) |
| return self._bin |
| |
| def build(self, builder=None): |
| """Build documentation with given builder.""" |
| if builder: |
| self._builder = builder |
| if sys.platform in ('win32', 'cygwin'): |
| command = [self.script] |
| else: |
| command = [sys.executable, '-u', '-B', self.script] |
| command += self.options + [self._rstdir, self.outdir] |
| execute(command, self.outdir, None, True) |
| |
| @property |
| def builder(self): |
| """The builder name. |
| |
| .. seealso:: http://www.sphinx-doc.org/en/stable/builders.html#builders |
| """ |
| return self._builder |
| |
| @property |
| def cachedir(self): |
| """The directory where cache files are generated.""" |
| if self._cachedir is None: |
| self._cachedir = fullpath((self.outdir, 'cache')) |
| return self._cachedir |
| |
| def install(self): |
| """Install Sphinx if needed.""" |
| # On Windows, install with pip, and assume setuptools is there. |
| if not self.script or not os.path.exists(self.script): |
| if sys.platform in ('win32', 'cygwin'): |
| self._install_windows() |
| else: |
| self._install_unix() |
| |
| @property |
| def installed(self): |
| return self.script and os.path.exists(self.script) |
| |
| @property |
| def lib(self): |
| """Sphinx python library path.""" |
| if self._lib is None: |
| pyversion = 'python' + str(sys.version_info[0]) + '.' + \ |
| str(sys.version_info[1]) |
| if sys.platform in ('win32', 'cygwin'): |
| self._lib = fullpath((sys.executable, '..', '..', 'Lib')) |
| else: |
| self._lib = fullpath((self.path, 'lib', pyversion)) |
| return self._lib |
| |
| @property |
| def options(self): |
| """The list of options used at doc generation time. |
| |
| :returns: A |list| of |str| representing the list of options used |
| by this Sphinx installation as doc generation time. |
| """ |
| _options = ['-d', self.cachedir, '-b', self.builder] |
| |
| if self.builder is self.BUILDER_HTML: |
| _options += ['-D', 'html_theme=' + self.theme] |
| |
| return _options |
| |
| @property |
| def outdir(self): |
| """Output directory for this documentation parser.""" |
| if self._outdir is None: |
| self._outdir = fullpath((self.path, self.builder)) |
| return self._outdir |
| |
| @outdir.setter |
| def outdir(self, value): |
| """Set the output directory for this documentation parser.""" |
| if value: |
| # create directory if possible. |
| value = fullpath(value) |
| if not os.path.exists(value): |
| try: |
| os.makedirs(value) |
| except Exception as e: |
| msg = 'Could not create directory "' + str(value) + \ |
| '". Got error : "' + str(e) + '".' |
| raise Exception(msg) |
| self._outdir = value |
| else: |
| self._outdir = None |
| |
| @property |
| def path(self): |
| """The path this Sphinx utility is installed in.""" |
| return self._path |
| |
| @property |
| def pythonpath(self): |
| """The python path to use sphinx. |
| |
| :returns: A |list| of python paths to use with this Sphinx |
| installation. |
| """ |
| if not hasattr(self, '_pythonpath') or self._pythonpath is None: |
| sitepkgs = fullpath((self.lib, 'site-packages')) |
| if sys.platform in ('win32', 'cygwin'): |
| # Use the python install on Windows. |
| self._pythonpath = [] |
| else: |
| self._pythonpath = [self.lib, sitepkgs] |
| try: |
| if not os.path.exists(self.lib): |
| os.makedirs(self.lib) |
| if not os.path.exists(sitepkgs): |
| os.makedirs(sitepkgs) |
| except OSError as ose: |
| # Check if it is a 'Permission denied' error. |
| if ose.errno == 13: |
| msg = 'Could not install Sphinx, you need to have '\ |
| 'administrator permissions, or use the -sphinxdir '\ |
| 'option.' |
| raise Exception(msg) |
| else: |
| raise ose |
| except Exception as e: |
| raise e |
| return self._pythonpath |
| |
| @property |
| def script(self): |
| """Path of this sphinx installation build script. |
| |
| :returns: An |str| representing the path to this Sphinx installation |
| build script, or **None** if the script could not be found. |
| """ |
| if not hasattr(self, '_script') or self._script is None or \ |
| not os.path.exists(self._script): |
| self._script = None |
| # Try to find it ... |
| if sys.platform in ('win32', 'cygwin'): |
| path = fullpath((self.bin, 'Scripts', 'sphinx-build.exe')) |
| else: |
| path = fullpath((self.bin, 'sphinx-build')) |
| |
| if os.path.exists(path): |
| self._script = path |
| |
| return self._script |
| |
| @property |
| def theme(self): |
| """The current theme for generated documentation.""" |
| return self._theme |
| |
| @theme.setter |
| def theme(self, value): |
| """Set the current theme for generated documentation.""" |
| if value in self.THEMES: |
| self._theme = value |
| elif value is None: |
| if ispython3: |
| self._theme = Sphinx.THEME_PYRAMID |
| else: |
| self._theme = self.THEME_DEFAULT |
| else: |
| raise Exception('Unknown Sphinx theme "' + value + |
| '". Please use one of: ' + ' ' .join(self.themes) + |
| '.') |
| |
| @property |
| def themes(self): |
| """The list of possible themes for documentation. |
| |
| :returns: A |tuple| of |str| listing the possible themes for this |
| Sphinx installation. |
| """ |
| return self.THEMES |
| |
| # -------------------------- protected methods -------------------------- # |
| |
| def _install_unix(self): |
| if not self.script or not os.path.exists(self.script): |
| # Go and install it. This may require a sudo from the user on |
| # Linux hosts. |
| |
| msg = 'Installing ' + self._name + ' in ' + self.path + '.\n' |
| msg += 'Getting ' + self._name + ' from "' + self._url_base + '".' |
| sys.stdout.write(msg + '\n') |
| |
| # Unset the PYTHONDONTWRITEBYTECODE variable if set, or some |
| # 'setup.py build' fail. |
| |
| savedenv = {} |
| if 'PYTHONDONTWRITEBYTECODE' in os.environ: |
| name = 'PYTHONDONTWRITEBYTECODE' |
| savedenv[name] = os.environ.pop(name) |
| |
| dldir = os.path.join(self.path, 'download') |
| instargs = (['build'], ['install', '--prefix', self._path]) |
| |
| # Check if setuptools is installed. |
| |
| try: |
| import setuptools # @UnresolvedImport @UnusedImport |
| except ImportError: |
| # Check if setuptools is in the site-packages. |
| sitepkgs = fullpath((self.lib, 'site-packages')) |
| if not glob.glob(os.path.join(sitepkgs, 'setuptools*')): |
| # Install it too. |
| sturl = 'https://pypi.io/packages/source/s/setuptools/'\ |
| 'setuptools-24.0.2.zip' |
| filepath = wget(sturl, dldir) |
| zf = zipfile.ZipFile(filepath, 'r') |
| zf.extractall(dldir) |
| zf.close() |
| # Run the build and install steps |
| stdir = fullpath((dldir, 'setuptools-24.0.2')) |
| |
| for arguments in instargs: |
| prcargs = [sys.executable, |
| fullpath((stdir, 'setup.py'))] |
| prcargs += arguments |
| msg = 'setuptools-24.0.2 ' + ' '.join(arguments) |
| execute(prcargs, stdir, msg) |
| |
| # Now that the setuptools package is here, we may install sphinx |
| # too. |
| |
| filepath = wget(self._url, dldir) |
| tar = tarfile.open(filepath) |
| os.chdir(dldir) |
| tar.extractall() |
| tar.close() |
| |
| # Run the build and install steps |
| |
| for arguments in instargs: |
| spdir = fullpath((dldir, self._name)) |
| prcargs = [sys.executable, fullpath((spdir, 'setup.py'))] |
| prcargs += arguments |
| msg = self._name + ' ' + ' '.join(arguments) |
| execute(prcargs, spdir, msg, True) |
| |
| # Restore modified env variables if needed |
| |
| for varname, value in savedenv.items(): |
| os.environ[varname] = value |
| |
| def _install_windows(self): |
| # With Windows hosts, assume setuptools is installed, and use pip to |
| # get Sphinx. |
| |
| python = fullpath(sys.executable) |
| pyinstall = os.path.dirname(python) |
| pip = fullpath((pyinstall, 'Scripts', 'pip.exe')) |
| |
| # Update pip if needed |
| |
| command = [python, '-m', 'pip', 'install', '--upgrade', 'pip'] |
| execute(command, pyinstall) |
| |
| # Install sphinx |
| |
| command = [pip, 'install', 'sphinx'] |
| execute(command, pyinstall) |
| |
| |
| class HtmlDocumentation(argparse.ArgumentParser): |
| |
| COMMAND_GENERATE = 'generate' |
| COMMAND_CLEAN = 'clean' |
| COMMAND_CLEANALL = 'cleanall' |
| COMMAND_DEFAULT = COMMAND_GENERATE |
| COMMANDS = (COMMAND_CLEAN, COMMAND_CLEANALL, COMMAND_GENERATE) |
| |
| """An Html documentation tool based on Sphinx.""" |
| def __init__(self): |
| self._command = None |
| self._sphinx = None |
| |
| # In order to install sphinx in a folder we are sure to be allowed to |
| # install, use this git repo in the: org.eclipse.tcf.python/docs |
| # folder. |
| |
| # Current __file__ is org.eclipse.tcf.python/src/docs/docs.py |
| self._outdir = fullpath((__file__, '..', '..', '..', 'docs', 'html')) |
| self._sphinxdir = fullpath((self._outdir, '..', 'sphinx')) |
| |
| super(HtmlDocumentation, self).__init__('Tool for TCF python HTML ' |
| 'documentation.') |
| |
| # Add command line options. |
| |
| self.add_argument('-outdir', |
| help='Directory to generate documentation into. ' |
| 'Default is "' + self._outdir + '".') |
| if sys.platform not in ('win32', 'cygwin'): |
| self.add_argument('-sphinxdir', |
| help='Directory to install sphinx into. ' |
| 'Default is "' + self._sphinxdir + '".') |
| self.add_argument('-theme', choices=self.sphinx.themes, |
| help='The sphinx theme used for generated ' |
| 'documentation.') |
| |
| self.add_argument('command', choices=self.COMMANDS, |
| help='The name of the command to execute.') |
| |
| @property |
| def command(self): |
| return self._command |
| |
| @command.setter |
| def command(self, value): |
| """Set the name of the current command.""" |
| if value in self.COMMANDS: |
| self._command = value |
| else: |
| self._command = self.COMMAND_DEFAULT |
| |
| def clean(self): |
| """Clean the output directory content. |
| |
| .. seealso:: |outdir| |
| """ |
| sys.stdout.write('Removing directory "' + str(self.outdir) + '" ... ') |
| if self.outdir and os.path.exists(self.outdir): |
| shutil.rmtree(self.outdir) |
| sys.stdout.write('done\n') |
| |
| def cleanall(self): |
| """Clean the output directory content and the sphinx directory content. |
| |
| .. seealso:: |outdir|, |sphinxdir| |
| """ |
| self.clean() |
| if sys.platform in ('win32', 'cygwin'): |
| # With Windows hosts, assume setuptools is installed, and use pip |
| # to remove Sphinx. |
| |
| python = fullpath(sys.executable) |
| pyinstall = os.path.dirname(python) |
| pip = fullpath((pyinstall, 'Scripts', 'pip.exe')) |
| |
| # Update pip if needed |
| |
| command = [python, '-m', 'pip', 'install', '--upgrade', 'pip'] |
| execute(command, pyinstall) |
| |
| # Install sphinx |
| |
| command = [pip, 'uninstall', '-y', 'sphinx'] |
| execute(command, pyinstall, None, True) |
| else: |
| sys.stdout.write('Removing directory "' + str(self.sphinxdir) + |
| '" ... ') |
| if self.sphinxdir and os.path.exists(self.sphinxdir): |
| shutil.rmtree(self.sphinxdir) |
| sys.stdout.write('done\n') |
| |
| def execute(self): |
| """Execute the current command.""" |
| getattr(self, self.command)() |
| |
| def generate(self): |
| """Command to generate html documentation.""" |
| # Override PYTHONPATH : insert TCF project path and sphinx install |
| # paths. |
| ppath = self.sphinx.pythonpath + sys.path |
| ppath.insert(0, fullpath('..')) |
| |
| os.environ['PYTHONPATH'] = os.pathsep.join(ppath) |
| |
| self.sphinx.install() |
| self.sphinx.outdir = self.outdir |
| self.sphinx.theme = self.theme |
| self.sphinx.build('html') |
| |
| @property |
| def outdir(self): |
| """Output directory for this documentation parser.""" |
| return self._outdir |
| |
| @outdir.setter |
| def outdir(self, value): |
| """Set the output directory for this documentation parser.""" |
| if value: |
| # create directory if possible. |
| value = fullpath(value) |
| if not os.path.exists(value): |
| try: |
| os.makedirs(value) |
| except Exception as e: |
| msg = 'Could not create directory "' + str(value) + \ |
| '". Got error : "' + str(e) + '".' |
| raise Exception(msg) |
| self._outdir = value |
| else: |
| self._outdir = None |
| |
| @property |
| def sphinx(self): |
| """The current sphinx installation definition.""" |
| if not self._sphinx: |
| self._sphinx = Sphinx(self.sphinxdir) |
| return self._sphinx |
| |
| @property |
| def sphinxdir(self): |
| """Sphinx installation directory.""" |
| return self._sphinxdir |
| |
| @sphinxdir.setter |
| def sphinxdir(self, value): |
| """Set the sphinx installation directory.""" |
| if value: |
| # create directory if possible. |
| value = fullpath(value) |
| if not os.path.exists(value): |
| try: |
| os.makedirs(value) |
| except Exception as e: |
| msg = 'Could not create directory "' + str(value) + \ |
| '". Got error : "' + str(e) + '".' |
| raise Exception(msg) |
| self._sphinxdir = value |
| self._sphinx = None |
| else: |
| self._sphinxdir = None |
| |
| @property |
| def theme(self): |
| """The current theme for the generated documentation.""" |
| if not hasattr(self, '_theme') or self._theme is None: |
| if ispython3: |
| self._theme = Sphinx.THEME_PYRAMID |
| else: |
| self._theme = Sphinx.THEME_DEFAULT |
| return self._theme |
| |
| @theme.setter |
| def theme(self, value): |
| """Set the current theme for generated documentation.""" |
| if value and value in Sphinx.THEMES: |
| self._theme = value |
| else: |
| self._theme = None |
| |
| |
| if __name__ == "__main__": |
| try: |
| doc = HtmlDocumentation() |
| doc.parse_args(namespace=doc) |
| doc.execute() |
| except Exception as e: |
| sys.stderr.write('Got exception "' + str(e) + '".') |