예제 #1
0
def read_config(source, current_name):
    """Read the Sphinx config for one version.

    :raise HandledError: If sphinx-build fails. Will be logged before raising.

    :param str source: Source directory to pass to sphinx-build.
    :param str current_name: The ref name of the current version being built.

    :return: Specific Sphinx config values.
    :rtype: dict
    """
    log = logging.getLogger(__name__)
    queue = multiprocessing.Queue()
    config = Config.from_context()

    with TempDir() as temp_dir:
        argv = ('sphinx-build', source, temp_dir)
        log.debug('Running sphinx-build for config values with args: %s',
                  str(argv))
        child = multiprocessing.Process(target=_read_config,
                                        args=(argv, config, current_name,
                                              queue))
        child.start()
        child.join()  # Block.
        if child.exitcode != 0:
            log.error(
                'sphinx-build failed for branch/tag while reading config: %s',
                current_name)
            raise HandledError

    config = queue.get()
    return config
예제 #2
0
def setup(app):
    """Called by Sphinx during phase 0 (initialization).

    :param sphinx.application.Sphinx app: Sphinx application object.

    :returns: Extension version.
    :rtype: dict
    """
    # Used internally. For rebuilding all pages when one or versions fail.
    app.add_config_value('sphinxcontrib_versioning_versions',
                         SC_VERSIONING_VERSIONS, 'html')

    # Needed for banner.
    app.config.html_static_path.append(STATIC_DIR)
    app.add_stylesheet('banner.css')

    # Tell Sphinx which config values can be set by the user.
    for name, default in Config():
        app.add_config_value('scv_{}'.format(name), default, 'html')

    # Event handlers.
    app.connect('builder-inited', EventHandlers.builder_inited)
    app.connect('env-updated', EventHandlers.env_updated)
    app.connect('html-page-context', EventHandlers.html_page_context)
    return dict(version=__version__)
예제 #3
0
def build(source, target, versions, current_name, is_root):
    """Build Sphinx docs for one version. Includes Versions class instance with names/urls in the HTML context.

    :raise HandledError: If sphinx-build fails. Will be logged before raising.

    :param str source: Source directory to pass to sphinx-build.
    :param str target: Destination directory to write documentation to (passed to sphinx-build).
    :param sphinxcontrib.versioning.versions.Versions versions: Versions class instance.
    :param str current_name: The ref name of the current version being built.
    :param bool is_root: Is this build in the web root?
    """
    log = logging.getLogger(__name__)
    argv = ('sphinx-build', source, target)
    config = Config.from_context()

    log.debug('Running sphinx-build for %s with args: %s', current_name,
              str(argv))
    child = multiprocessing.Process(target=_build,
                                    args=(argv, config, versions, current_name,
                                          is_root))
    child.start()
    child.join()  # Block.
    if child.exitcode != 0:
        log.error('sphinx-build failed for branch/tag: %s', current_name)
        raise HandledError
예제 #4
0
def read_config(source, current_name):
    """Read the Sphinx config for one version.

    :raise HandledError: If sphinx-build fails. Will be logged before raising.

    :param str source: Source directory to pass to sphinx-build.
    :param str current_name: The ref name of the current version being built.

    :return: Specific Sphinx config values.
    :rtype: dict
    """
    log = logging.getLogger(__name__)
    queue = multiprocessing.Queue()
    config = Config.from_context()

    with TempDir() as temp_dir:
        argv = ('sphinx-build', source, temp_dir)
        log.debug('Running sphinx-build for config values with args: %s', str(argv))
        child = multiprocessing.Process(target=_read_config, args=(argv, config, current_name, queue))
        child.start()
        child.join()  # Block.
        if child.exitcode != 0:
            log.error('sphinx-build failed for branch/tag while reading config: %s', current_name)
            raise HandledError

    config = queue.get()
    return config
예제 #5
0
def build_all(exported_root, destination, versions):
    """Build all versions.

    :param str exported_root: Tempdir path with exported commits as subdirectories.
    :param str destination: Destination directory to copy/overwrite built docs to. Does not delete old files.
    :param sphinxcontrib.versioning.versions.Versions versions: Versions class instance.
    """
    log = logging.getLogger(__name__)

    while True:
        # Build root.
        remote = versions[Config.from_context().root_ref]
        log.info('Building root: %s', remote['name'])
        source = os.path.dirname(
            os.path.join(exported_root, remote['sha'],
                         remote['conf_rel_path']))
        build(source, destination, versions, remote['name'], True)

        # Build all refs.
        for remote in list(versions.remotes):
            log.info('Building ref: %s', remote['name'])
            source = os.path.dirname(
                os.path.join(exported_root, remote['sha'],
                             remote['conf_rel_path']))
            target = os.path.join(destination, remote['root_dir'])
            try:
                build(source, target, versions, remote['name'], False)
            except HandledError:
                log.warning(
                    'Skipping. Will not be building %s. Rebuilding everything.',
                    remote['name'])
                versions.remotes.pop(versions.remotes.index(remote))
                break  # Break out of for loop.
        else:
            break  # Break out of while loop if for loop didn't execute break statement above.
예제 #6
0
def build_all(exported_root, destination, versions):
    """Build all versions.

    :param str exported_root: Tempdir path with exported commits as subdirectories.
    :param str destination: Destination directory to copy/overwrite built docs to. Does not delete old files.
    :param sphinxcontrib.versioning.versions.Versions versions: Versions class instance.
    """
    log = logging.getLogger(__name__)

    while True:
        # Build root.
        remote = versions[Config.from_context().root_ref]
        log.info('Building root: %s', remote['name'])
        source = os.path.dirname(os.path.join(exported_root, remote['sha'], remote['conf_rel_path']))
        build(source, destination, versions, remote['name'], True)

        # Build all refs.
        for remote in list(versions.remotes):
            log.info('Building ref: %s', remote['name'])
            source = os.path.dirname(os.path.join(exported_root, remote['sha'], remote['conf_rel_path']))
            target = os.path.join(destination, remote['root_dir'])
            try:
                build(source, target, versions, remote['name'], False)
            except HandledError:
                log.warning('Skipping. Will not be building %s. Rebuilding everything.', remote['name'])
                versions.remotes.pop(versions.remotes.index(remote))
                break  # Break out of for loop.
        else:
            break  # Break out of while loop if for loop didn't execute break statement above.
def pre_build(local_root, versions):
    """Build docs for all versions to determine root directory and master_doc names.

    Need to build docs to (a) avoid filename collision with files from root_ref and branch/tag names and (b) determine
    master_doc config values for all versions (in case master_doc changes from e.g. contents.rst to index.rst between
    versions).

    Exports all commits into a temporary directory and returns the path to avoid re-exporting during the final build.

    :param str local_root: Local path to git root directory.
    :param sphinxcontrib.versioning.versions.Versions versions: Versions class instance.

    :return: Tempdir path with exported commits as subdirectories.
    :rtype: str
    """
    log = logging.getLogger(__name__)
    exported_root = TempDir(True).name

    # Extract all.
    for sha in {r['sha'] for r in versions.remotes}:
        target = os.path.join(exported_root, sha)
        log.debug('Exporting %s to temporary directory.', sha)
        export(local_root, sha, target)

    # Build root.
    remote = versions[Config.from_context().root_ref]
    with TempDir() as temp_dir:
        log.debug('Building root (before setting root_dirs) in temporary directory: %s', temp_dir)
        source = os.path.dirname(os.path.join(exported_root, remote['sha'], remote['conf_rel_path']))
        build(source, temp_dir, versions, remote['name'], True)
        existing = os.listdir(temp_dir)

    # Define root_dir for all versions to avoid file name collisions.
    for remote in versions.remotes:
        root_dir = RE_INVALID_FILENAME.sub('_', remote['name'])
        while root_dir in existing:
            root_dir += '_'
        remote['root_dir'] = root_dir
        log.debug('%s root directory is %s', remote['name'], root_dir)
        existing.append(root_dir)

    # Get found_docs and master_doc values for all versions.
    for remote in list(versions.remotes):
        log.debug('Partially running sphinx-build to read configuration for: %s', remote['name'])
        source = os.path.dirname(os.path.join(exported_root, remote['sha'], remote['conf_rel_path']))
        try:
            config = read_config(source, remote['name'])
        except HandledError:
            log.warning('Skipping. Will not be building: %s', remote['name'])
            versions.remotes.pop(versions.remotes.index(remote))
            continue
        remote['found_docs'] = config['found_docs']
        remote['master_doc'] = config['master_doc']

    return exported_root
예제 #8
0
def config(monkeypatch):
    """Mock config from Click context.

    :param monkeypatch: pytest fixture.

    :return: Config instance.
    :rtype: sphinxcontrib.versioning.lib.Config
    """
    instance = Config()
    ctx = type('', (), {'find_object': staticmethod(lambda _: instance)})
    monkeypatch.setattr('click.get_current_context', lambda: ctx)
    return instance
예제 #9
0
def build(source, target, versions, current_name, is_root):
    """Build Sphinx docs for one version. Includes Versions class instance with names/urls in the HTML context.

    :raise HandledError: If sphinx-build fails. Will be logged before raising.

    :param str source: Source directory to pass to sphinx-build.
    :param str target: Destination directory to write documentation to (passed to sphinx-build).
    :param sphinxcontrib.versioning.versions.Versions versions: Versions class instance.
    :param str current_name: The ref name of the current version being built.
    :param bool is_root: Is this build in the web root?
    """
    log = logging.getLogger(__name__)
    argv = ('sphinx-build', source, target)
    config = Config.from_context()

    log.debug('Running sphinx-build for %s with args: %s', current_name, str(argv))
    child = multiprocessing.Process(target=_build, args=(argv, config, versions, current_name, is_root))
    child.start()
    child.join()  # Block.
    if child.exitcode != 0:
        log.error('sphinx-build failed for branch/tag: %s', current_name)
        raise HandledError
def build(source, target, versions, current_name, is_root):
    """Build Sphinx docs for one version. Includes Versions class instance with names/urls in the HTML context.

    :raise HandledError: If sphinx-build fails. Will be logged before raising.

    :param str source: Source directory to pass to sphinx-build.
    :param str target: Destination directory to write documentation to (passed to sphinx-build).
    :param sphinxcontrib.versioning.versions.Versions versions: Versions class instance.
    :param str current_name: The ref name of the current version being built.
    :param bool is_root: Is this build in the web root?
    """
    import subprocess

    log = logging.getLogger(__name__)
    argv = ('sphinx-build', source, target)
    config = Config.from_context()

    if config.run_setup_py:
        with ChangeDir(source):
            current_version_root = subprocess.check_output(
                "git rev-parse --show-toplevel".split(" ")).decode(
                    "utf-8").split("\n")[0]

        with ChangeDir(current_version_root):
            subprocess.check_call(["python", "setup.py", "install"])

    log.debug('Running sphinx-build for %s with args: %s', current_name,
              str(argv))
    child = multiprocessing.Process(target=_build,
                                    args=(argv, config, versions, current_name,
                                          is_root))
    child.start()
    child.join()  # Block.
    if child.exitcode != 0:
        log.error('sphinx-build failed for branch/tag: %s', current_name)
        raise HandledError
예제 #11
0
def test_config():
    """Test Config."""
    config = Config()
    config.update(
        dict(invert=True,
             overflow=('-D', 'key=value'),
             root_ref='master',
             verbose=1))

    # Verify values.
    assert config.banner_main_ref == 'master'
    assert config.greatest_tag is False
    assert config.invert is True
    assert config.overflow == ('-D', 'key=value')
    assert config.root_ref == 'master'
    assert config.verbose == 1
    assert repr(config) == (
        "<sphinxcontrib.versioning.lib.Config "
        "_program_state={}, verbose=1, root_ref='master', overflow=('-D', 'key=value')>"
    )

    # Verify iter.
    actual = sorted(config)
    expected = [
        ('banner_greatest_tag', False),
        ('banner_main_ref', 'master'),
        ('banner_recent_tag', False),
        ('chdir', None),
        ('git_root', None),
        ('greatest_tag', False),
        ('grm_exclude', tuple()),
        ('invert', True),
        ('local_conf', None),
        ('no_colors', False),
        ('no_local_conf', False),
        ('overflow', ('-D', 'key=value')),
        ('priority', None),
        ('push_remote', 'origin'),
        ('recent_tag', False),
        ('root_ref', 'master'),
        ('show_banner', False),
        ('sort', tuple()),
        ('verbose', 1),
        ('whitelist_branches', tuple()),
        ('whitelist_tags', tuple()),
    ]
    assert actual == expected

    # Verify contains, setitem, and pop.
    assert getattr(config, '_program_state') == dict()
    assert 'key' not in config
    config['key'] = 'value'
    assert getattr(config, '_program_state') == dict(key='value')
    assert 'key' in config
    assert config.pop('key') == 'value'
    assert getattr(config, '_program_state') == dict()
    assert 'key' not in config
    assert config.pop('key', 'nope') == 'nope'
    assert getattr(config, '_program_state') == dict()
    assert 'key' not in config

    # Test exceptions.
    with pytest.raises(AttributeError) as exc:
        config.update(dict(unknown=True))
    assert exc.value.args[0] == "'Config' object has no attribute 'unknown'"
    with pytest.raises(AttributeError) as exc:
        config.update(dict(_program_state=dict(key=True)))
    assert exc.value.args[
        0] == "'Config' object does not support item assignment on '_program_state'"
    with pytest.raises(AttributeError) as exc:
        config.update(dict(invert=False))
    assert exc.value.args[
        0] == "'Config' object does not support item re-assignment on 'invert'"
예제 #12
0
def test_config():
    """Test Config."""
    config = Config()
    config.update(dict(invert=True, overflow=('-D', 'key=value'), root_ref='master', verbose=1))

    # Verify values.
    assert config.banner_main_ref == 'master'
    assert config.greatest_tag is False
    assert config.invert is True
    assert config.overflow == ('-D', 'key=value')
    assert config.root_ref == 'master'
    assert config.verbose == 1
    assert repr(config) == ("<sphinxcontrib.versioning.lib.Config "
                            "_program_state={}, verbose=1, root_ref='master', overflow=('-D', 'key=value')>")

    # Verify iter.
    actual = sorted(config)
    expected = [
        ('banner_greatest_tag', False),
        ('banner_main_ref', 'master'),
        ('banner_recent_tag', False),
        ('chdir', None),
        ('git_root', None),
        ('greatest_tag', False),
        ('grm_exclude', tuple()),
        ('invert', True),
        ('local_conf', None),
        ('no_colors', False),
        ('no_local_conf', False),
        ('overflow', ('-D', 'key=value')),
        ('priority', None),
        ('push_remote', 'origin'),
        ('recent_tag', False),
        ('root_ref', 'master'),
        ('show_banner', False),
        ('sort', tuple()),
        ('verbose', 1),
        ('whitelist_branches', tuple()),
        ('whitelist_tags', tuple()),
    ]
    assert actual == expected

    # Verify contains, setitem, and pop.
    assert getattr(config, '_program_state') == dict()
    assert 'key' not in config
    config['key'] = 'value'
    assert getattr(config, '_program_state') == dict(key='value')
    assert 'key' in config
    assert config.pop('key') == 'value'
    assert getattr(config, '_program_state') == dict()
    assert 'key' not in config
    assert config.pop('key', 'nope') == 'nope'
    assert getattr(config, '_program_state') == dict()
    assert 'key' not in config

    # Test exceptions.
    with pytest.raises(AttributeError) as exc:
        config.update(dict(unknown=True))
    assert exc.value.args[0] == "'Config' object has no attribute 'unknown'"
    with pytest.raises(AttributeError) as exc:
        config.update(dict(_program_state=dict(key=True)))
    assert exc.value.args[0] == "'Config' object does not support item assignment on '_program_state'"
    with pytest.raises(AttributeError) as exc:
        config.update(dict(invert=False))
    assert exc.value.args[0] == "'Config' object does not support item re-assignment on 'invert'"