diff --git a/readthedocs_build/config/config.py b/readthedocs_build/config/config.py index a69a1e1..6b4f85f 100644 --- a/readthedocs_build/config/config.py +++ b/readthedocs_build/config/config.py @@ -6,7 +6,8 @@ from .parser import ParseError from .parser import parse from .validation import (validate_bool, validate_choice, validate_directory, - validate_file, validate_string, ValidationError) + validate_file, validate_list, validate_string, + ValidationError) __all__ = ( @@ -14,11 +15,14 @@ CONFIG_FILENAMES = ('readthedocs.yml', '.readthedocs.yml') +ALL = 'all' BASE_INVALID = 'base-invalid' BASE_NOT_A_DIR = 'base-not-a-directory' CONFIG_SYNTAX_INVALID = 'config-syntax-invalid' +INVALID_KEYS_COMBINATION = 'invalid-keys-combination' +MISSING_REQUIRED_KEY = 'missing-required-key' CONFIG_REQUIRED = 'config-required' NAME_REQUIRED = 'name-required' NAME_INVALID = 'name-invalid' @@ -168,6 +172,7 @@ def validate(self): self.validate_base() self.validate_python() self.validate_formats() + self.validate_submodules() self.validate_conda() self.validate_requirements_file() @@ -408,6 +413,66 @@ def validate_formats(self): return True + def validate_submodules(self): + raw_submodules = self.raw_config.get('submodules') + if raw_submodules is None: + self['submodules'] = self.get_default_submodules_config() + return None + + if 'include' in raw_submodules and 'exclude' in raw_submodules: + self.error( + 'submodules', + 'It is not possible to include and exclude submodules', + code=INVALID_KEYS_COMBINATION + ) + + self['submodules'] = {} + submodules = self['submodules'] + + with self.catch_validation_error('submodules.recursive'): + recursive = raw_submodules.get('recursive', False) + submodules['recursive'] = validate_bool(recursive) + + if 'include' in raw_submodules: + with self.catch_validation_error('submodules.include'): + include = raw_submodules['include'] + if include == ALL: + submodules['include'] = ALL + else: + submodules['include'] = validate_list(include) + self.validate_directories(submodules['include']) + if not submodules['include']: + submodules['recursive'] = False + elif 'exclude' in raw_submodules: + with self.catch_validation_error('submodules.exclude'): + exclude = raw_submodules['exclude'] + if exclude == ALL: + submodules['exclude'] = ALL + submodules['recursive'] = False + else: + submodules['exclude'] = validate_list(exclude) + self.validate_directories(submodules['exclude']) + elif submodules['recursive'] is True: + self.error( + 'submodules', + 'You need to include submodules', + code=MISSING_REQUIRED_KEY + ) + return True + + def get_default_submodules_config(self): + return { + 'include': [], + 'recursive': False, + } + + def validate_directories(self, directories): + base_path = os.path.dirname(self.source_file) + return [ + validate_directory(directory, base_path) + for directory in directories + ] + class ProjectConfig(list): diff --git a/readthedocs_build/config/test_config.py b/readthedocs_build/config/test_config.py index 8d14a73..72d2c30 100644 --- a/readthedocs_build/config/test_config.py +++ b/readthedocs_build/config/test_config.py @@ -9,6 +9,9 @@ from .config import load from .config import BuildConfig from .config import ProjectConfig +from .config import ALL +from .config import INVALID_KEYS_COMBINATION +from .config import MISSING_REQUIRED_KEY from .config import TYPE_REQUIRED from .config import NAME_REQUIRED from .config import NAME_INVALID @@ -16,6 +19,7 @@ from .validation import INVALID_BOOL from .validation import INVALID_CHOICE from .validation import INVALID_DIRECTORY +from .validation import INVALID_LIST from .validation import INVALID_PATH from .validation import INVALID_STRING @@ -349,6 +353,204 @@ def it_uses_validate_file(tmpdir): assert args[0] == 'setup.py' +def describe_validate_submodules(): + + def it_defaults_to_all_submodules(tmpdir): + with tmpdir.as_cwd(): + source_file = tmpdir.join('subdir', 'readthedocs.yml') + build = get_build_config({}, source_file=str(source_file)) + build.validate_submodules() + # TODO: This will be the dafult behavior? + assert 'submodules' in build + assert build['submodules']['include'] == [] + assert build['submodules']['recursive'] is False + + def it_parses_include_key(tmpdir): + apply_fs(tmpdir, {'sub-one': {}, 'sub-two': {}}) + with tmpdir.as_cwd(): + build = get_build_config({ + 'submodules': { + 'include': ['sub-one/', 'sub-two'], + } + }) + build.validate_submodules() + assert 'submodules' in build + assert 'exclude' not in build['submodules'] + assert build['submodules']['include'] == ['sub-one/', 'sub-two'] + assert build['submodules']['recursive'] is False + + def it_validates_include_key(tmpdir): + apply_fs(tmpdir, {'sub-one': {}, 'sub-two': {}}) + with tmpdir.as_cwd(): + build = get_build_config({ + 'submodules': { + 'include': ['sub-one/', 'sub-too'], + } + }) + with raises(InvalidConfig) as excinfo: + build.validate_submodules() + assert excinfo.value.key == 'submodules.include' + assert excinfo.value.code == INVALID_PATH + + def it_parses_exclude_key(tmpdir): + apply_fs(tmpdir, {'sub-one': {}, 'sub-two': {}}) + with tmpdir.as_cwd(): + build = get_build_config({ + 'submodules': { + 'exclude': ['sub-two'], + } + }) + build.validate_submodules() + assert 'submodules' in build + assert 'include' not in build['submodules'] + assert build['submodules']['exclude'] == ['sub-two'] + assert build['submodules']['recursive'] is False + + def it_validates_exclude_key(tmpdir): + apply_fs(tmpdir, {'sub-one': {}, 'sub-two': {}}) + with tmpdir.as_cwd(): + build = get_build_config({ + 'submodules': { + 'exclude': ['sub-too'], + } + }) + with raises(InvalidConfig) as excinfo: + build.validate_submodules() + assert excinfo.value.key == 'submodules.exclude' + assert excinfo.value.code == INVALID_PATH + + def it_does_not_allow_exclude_and_include(tmpdir): + build = get_build_config({ + 'submodules': { + 'include': ['sub-one'], + 'exclude': ['sub-two'], + } + }) + with raises(InvalidConfig) as excinfo: + build.validate_submodules() + assert excinfo.value.key == 'submodules' + assert excinfo.value.code == INVALID_KEYS_COMBINATION + + def it_requires_include_with_recursive(tmpdir): + build = get_build_config({ + 'submodules': { + 'recursive': True, + } + }) + with raises(InvalidConfig) as excinfo: + build.validate_submodules() + assert excinfo.value.key == 'submodules' + assert excinfo.value.code == MISSING_REQUIRED_KEY + + def it_parses_recursive_key(tmpdir): + apply_fs(tmpdir, {'sub-one': {}, 'sub-two': {}}) + with tmpdir.as_cwd(): + build = get_build_config({ + 'submodules': { + 'exclude': ['sub-two'], + 'recursive': True, + }, + }) + build.validate_submodules() + assert build['submodules']['recursive'] is True + + def it_validates_recursive_key(tmpdir): + apply_fs(tmpdir, {'sub-one': {}, 'sub-two': {}}) + with tmpdir.as_cwd(): + build = get_build_config({ + 'submodules': { + 'exclude': ['sub-two'], + 'recursive': 'True', + }, + }) + with raises(InvalidConfig) as excinfo: + build.validate_submodules() + assert excinfo.value.key == 'submodules.recursive' + assert excinfo.value.code == INVALID_BOOL + + def it_accepts_all_keyword_on_include(tmpdir): + apply_fs(tmpdir, {'sub-one': {}, 'sub-two': {}}) + with tmpdir.as_cwd(): + build = get_build_config({ + 'submodules': { + 'include': 'all', + 'recursive': True, + }, + }) + build.validate_submodules() + assert build['submodules']['include'] == ALL + assert build['submodules']['recursive'] is True + + def it_validates_the_correct_type_for_include(tmpdir): + apply_fs(tmpdir, {'sub-one': {}, 'sub-two': {}}) + with tmpdir.as_cwd(): + build = get_build_config({ + 'submodules': { + 'include': 'none', + 'recursive': True, + }, + }) + with raises(InvalidConfig) as excinfo: + build.validate_submodules() + assert excinfo.value.key == 'submodules.include' + assert excinfo.value.code == INVALID_LIST + + def it_accepts_all_keyword_on_exclude(tmpdir): + apply_fs(tmpdir, {'sub-one': {}, 'sub-two': {}}) + with tmpdir.as_cwd(): + build = get_build_config({ + 'submodules': { + 'exclude': 'all', + 'recursive': True, + }, + }) + build.validate_submodules() + assert build['submodules']['exclude'] == ALL + assert build['submodules']['recursive'] is False + + def it_validates_the_correct_type_for_exclude(tmpdir): + apply_fs(tmpdir, {'sub-one': {}, 'sub-two': {}}) + with tmpdir.as_cwd(): + build = get_build_config({ + 'submodules': { + 'exclude': 'none', + 'recursive': True, + }, + }) + with raises(InvalidConfig) as excinfo: + build.validate_submodules() + assert excinfo.value.key == 'submodules.exclude' + assert excinfo.value.code == INVALID_LIST + + def it_parses_explicit_defaults(tmpdir): + apply_fs(tmpdir, {'sub-one': {}, 'sub-two': {}}) + with tmpdir.as_cwd(): + build = get_build_config({ + 'submodules': { + 'include': [], + 'recursive': False, + } + }) + build.validate_submodules() + assert 'submodules' in build + assert build['submodules']['include'] == [] + assert build['submodules']['recursive'] is False + + def it_parses_explicit_wrong_defaults(tmpdir): + apply_fs(tmpdir, {'sub-one': {}, 'sub-two': {}}) + with tmpdir.as_cwd(): + build = get_build_config({ + 'submodules': { + 'include': [], + 'recursive': True, + } + }) + build.validate_submodules() + assert 'submodules' in build + assert build['submodules']['include'] == [] + assert build['submodules']['recursive'] is False + + def test_valid_build_config(): build = BuildConfig(env_config, minimal_config,