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
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__)
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 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
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 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
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
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
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'"
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'"