hob / hob / _scope.py

from os import makedirs
from os.path import exists, isdir, join, abspath, isfile, isabs, dirname, basename
from os import listdir, unlink
from subprocess import check_call, Popen, PIPE, CalledProcessError
import sys, optparse
from distutils.version import LooseVersion as Version

from hob.cmd import CommandTable
from hob.utils import _, tempdir
from hob.extension import ExtensionError
from hob.cmd import ProgramError
from hob.proto import Config

cmds = CommandTable()

def cmdLines(text):
    lines = text.splitlines()
    if lines:
        yield "> ", "", lines[0]
        for line in lines[1:]:
            yield "  ", " \\", line

def printCmd(text):
    for pre, post, line in cmdLines(text):
        print pre + line + post

def call(args, **kwargs):
    if "cwd" in kwargs and kwargs["cwd"]:
        printCmd("cd %s" % kwargs["cwd"])
    printCmd(" ".join(args))
    check_call(args, **kwargs)

def capture(args, **kwargs):
    if "stdout" not in kwargs and "stderr" not in kwargs:
        kwargs["stdout"] = PIPE
        kwargs["stderr"] = PIPE
    if "cwd" in kwargs and kwargs["cwd"]:
        printCmd("cd %s;" % kwargs["cwd"])
    #printCmd(" ".join(args + ["1>", "2>"]))
    p = Popen(args, **kwargs)
    out, err = p.communicate()
    if p.returncode != 0:
        CalledProcessError(p.returncode, args)
    return out, err

class Context(object):
    def __init__(self, cwd):
        self.cwd = cwd

    def call(self, args, **kwargs):
        return call(args, cwd=self.cwd, **kwargs)

    def capture(self, args, **kwargs):
        return capture(args, cwd=self.cwd, **kwargs)

def walkParents(path):
    if not isdir(path):
        path = dirname(path)
    while path:
        yield path
        parent = dirname(path)
        if parent == path or not parent:
            break
        path = parent


def isHgDir(path):
    for root in walkParents(path):
        if exists(join(root, ".hg")):
            return True
    return False

class DocGenerator(object):
    def __init__(self):
        self.sectionCount = 0
        self.projects = {}
        self.root = None
        self.out_dir = None
        self.build = None
        self.build_dir = None
        self.target = None
        self.specs = None
        self.specs_dir = None
        self.doc_dir = None
        self.service_source_dir = "services"
        self.service_source_path = None

    def init(self):
        for path in (self.build, self.out_dir):
            if not isdir(path):
                makedirs(path)

        self.specs = self.projects["specs"]["context"]
        self.specs_dir = self.projects["specs"]["build"] or self.projects["specs"]["path"]
        self.doc_dir = join(self.specs_dir, "doc")
        if isabs(self.service_source_dir):
            self.service_source_path = self.service_source_dir
        else:
            self.service_source_path = join(self.specs_dir, self.service_source_dir)
        self.service_out_path = join(self.doc_dir, "services")

        self.updateRepos()

        if isHgDir(self.out_dir):
            self.updateDoc(self.out_dir)

    def updateRepos(self):
        # Sync working directories from hg repos
        for project in self.projects.itervalues():
            if "repo" in project:
                self.newSection("SYNC %s" % project["repo"])
                self.sync(project["context"], project["repo"])

    def newSection(self, title):
        if self.sectionCount > 0:
            print
        print "===== %s =====" % title
        self.sectionCount += 1

    def createServices(self):
        # Check version of hob to determine options to use
        hob_opts = []
        rst_opts = []
        hob_version = capture(["hob", "--version"])[1].splitlines()[0]
        project_path = join(abspath(self.service_source_path), "hob.conf")
        if Version(hob_version) > "0.1":
            hob_opts = ["--project", project_path]
            rst_opts = ["--syntax", self.syntax]
        else:
            hob_opts = ["--config-file", project_path]
        if not self.target:
            project_conf = Config()
            project_conf.read([project_path])
            if ('hob', 'target') in project_conf:
                self.target = project_conf[('hob', 'target')]
        if self.target:
            hob_opts += ["--target", self.target]

        if self.clean_service_dir:
            self.newSection("CLEAN SERVICES %s" % self.service_out_path)
            # Remove old service files
            if self.service_out_path and exists(self.service_out_path):
                for fname in listdir(self.service_out_path):
                    fpath = join(self.service_out_path, fname)
                    if isfile(fpath):
                        unlink(fpath)
        # Create rst-docs from .proto files using hob
        self.newSection("SERVICES %s" % self.service_out_path)
        # TODO: It would been preferred if we could execute rst-doc directly, but it requires the ability
        #       to clone the current hob instance and setting basic settings such as --project
        call(["hob"] + hob_opts + ["rst-doc", "--out-dir", self.service_out_path] + rst_opts)

    def createDoc(self):
        # Create documentation with sphinx
        for sphinx_builder in self.sphinx_builders:
            self.newSection("SPHINX %s" % self.out_dir)
            self.specs.call([self.sphinx, "-b", sphinx_builder, "-c", self.doc_dir, "-d", join(self.out_dir, "doctrees"), self.doc_dir, self.out_dir])

    def createIndex(self, index_script):
        if exists(index_script):
            script_dir = dirname(index_script)
            script_name = basename(index_script)
            self.newSection("INDEX %s" % script_dir)
            call([sys.executable, script_name], cwd=script_dir)

    def sync(self, context, root):
        if exists(context.cwd):
            context.call(["hg", "up", "-C"])
            context.call(["hg", "pull", "-u", root])
        else:
            call(["hg", "clone", root, context.cwd])

    def updateDoc(self, path):
        self.newSection("PULL UPSTREAM %s" % path)
        context = Context(path)
        context.call(["hg", "pull", "-u"])

    def commitDoc(self, context, message=None):
        if not message:
            message = "Updated scope documentation"
        self.newSection("COMMIT %s" % (context.cwd or ''))
        context.call(["hg", "commit", "-m", message, "--addremove"])

    def pushDoc(self, context, dest=None):
        args = []
        if dest:
            args.append(dest)
        self.newSection("PUSH %s" % (context.cwd or ''))
        context.call(["hg", "push"] + args)

def repoProject(repo, build):
    if isdir(repo):
        repo = abspath(repo)
    proj = dict(build=build,
                repo=repo,
                )
    proj["context"] = Context(proj["build"])
    return proj

def pathProject(path):
    proj = dict(build=None,
                path=path,
                )
    proj["context"] = Context(proj["build"])
    return proj

def scope_doc(ui, temp_dir, root, scope_doc, target,
              specs_dir, specs_repo, index_script,
              sphinx_cmd,
              service_path, service_cleanup,
              commit, commit_message, push, push_dest,
              **kwargs):
    """Generate documentation for the scope protocol and related services.
    """
    gen = DocGenerator()

    if not root:
        raise ProgramError("No root specified, cannot create documentation without it.")
    gen.root = root
    gen.out_dir = abspath(gen.root)
    if scope_doc:
        if isabs(scope_doc):
            gen.out_dir = scope_doc
        else:
            gen.out_dir = join(gen.out_dir, scope_doc)

    if not temp_dir:
        temp_dir = tempdir(["scope-doc"])
    gen.build = gen.build_dir = temp_dir
    gen.target = target
    gen.syntax = 'scope'
    gen.sphinx = sphinx_cmd
    gen.sphinx_builders = ["html"]
    
    gen.clean_service_dir = service_cleanup

    gen.projects = {}

    if service_path:
        gen.service_source_dir = service_path

    if not specs_repo and not specs_dir:
        if exists("doc/conf.py") and exists("services/hob.conf"):
            specs_dir = ""
            gen.service_source_dir = "services"
        else:
            specs_repo = "https://bitbucket.org/scope/scope-services"

    if specs_repo:
        gen.projects["specs"] = repoProject(specs_repo, abspath(join(gen.build, "specs")))
    else:
        gen.projects["specs"] = pathProject(specs_dir)

    gen.init()

    gen.createServices()
    gen.createDoc()

    if not index_script:
        index = abspath(join(root, "makeindex.py"))
        if exists(index):
            index_script = index
    if index_script:
        gen.createIndex(index_script)

    if commit:
        doc_context = Context(gen.root)
        # TODO: Should figure out if there are any modified files present and only commit when modified
        gen.commitDoc(doc_context, commit_message)
        if push:
            gen.pushDoc(doc_context, push_dest)

    gen.newSection("FINISHED")
    print "Documentation has been generated in %s" % gen.out_dir
Tip: Filter by directory path e.g. /media app.js to search for public/media/app.js.
Tip: Use camelCasing e.g. ProjME to search for ProjectModifiedEvent.java.
Tip: Filter by extension type e.g. /repo .js to search for all .js files in the /repo directory.
Tip: Separate your search with spaces e.g. /ssh pom.xml to search for src/ssh/pom.xml.
Tip: Use ↑ and ↓ arrow keys to navigate and return to view the file.
Tip: You can also navigate files with Ctrl+j (next) and Ctrl+k (previous) and view the file with Ctrl+o.
Tip: You can also navigate files with Alt+j (next) and Alt+k (previous) and view the file with Alt+o.