diff --git a/readthedocs/config/config.py b/readthedocs/config/config.py index bcecddecae6..4b7c48f5649 100644 --- a/readthedocs/config/config.py +++ b/readthedocs/config/config.py @@ -36,7 +36,6 @@ 'ConfigError', 'ConfigOptionNotSupportedError', 'InvalidConfig', - 'ProjectConfig', ) ALL = 'all' @@ -110,12 +109,10 @@ class InvalidConfig(ConfigError): message_template = 'Invalid "{key}": {error}' - def __init__(self, key, code, error_message, source_file=None, - source_position=None): + def __init__(self, key, code, error_message, source_file=None): self.key = key self.code = code self.source_file = source_file - self.source_position = source_position message = self.message_template.format( key=key, code=code, @@ -149,11 +146,10 @@ class BuildConfigBase(object): ] version = None - def __init__(self, env_config, raw_config, source_file, source_position): + def __init__(self, env_config, raw_config, source_file): self.env_config = env_config self.raw_config = raw_config self.source_file = source_file - self.source_position = source_position if os.path.isdir(self.source_file): self.base_path = self.source_file else: @@ -165,10 +161,7 @@ def __init__(self, env_config, raw_config, source_file, source_position): def error(self, key, message, code): """Raise an error related to ``key``.""" if not os.path.isdir(self.source_file): - source = '{file} [{pos}]'.format( - file=os.path.relpath(self.source_file, self.base_path), - pos=self.source_position, - ) + source = os.path.relpath(self.source_file, self.base_path) error_message = '{source}: {message}'.format( source=source, message=message, @@ -180,7 +173,6 @@ def error(self, key, message, code): code=code, error_message=error_message, source_file=self.source_file, - source_position=self.source_position, ) @contextmanager @@ -194,7 +186,6 @@ def catch_validation_error(self, key): code=error.code, error_message=str(error), source_file=self.source_file, - source_position=self.source_position, ) def pop(self, name, container, default, raise_ex): @@ -1058,16 +1049,6 @@ def submodules(self): return Submodules(**self._config['submodules']) -class ProjectConfig(list): - - """Wrapper for multiple build configs.""" - - def validate(self): - """Validates each configuration build.""" - for build in self: - build.validate() - - def load(path, env_config): """ Load a project configuration and the top-most build config for a given path. @@ -1083,10 +1064,9 @@ def load(path, env_config): 'No configuration file found', code=CONFIG_REQUIRED ) - build_configs = [] with open(filename, 'r') as configuration_file: try: - configs = parse(configuration_file.read()) + config = parse(configuration_file.read()) except ParseError as error: raise ConfigError( 'Parse error in {filename}: {message}'.format( @@ -1095,23 +1075,19 @@ def load(path, env_config): ), code=CONFIG_SYNTAX_INVALID, ) - for i, config in enumerate(configs): - allow_v2 = env_config.get('allow_v2') - if allow_v2: - version = config.get('version', 1) - else: - version = 1 - build_config = get_configuration_class(version)( - env_config, - config, - source_file=filename, - source_position=i, - ) - build_configs.append(build_config) + allow_v2 = env_config.get('allow_v2') + if allow_v2: + version = config.get('version', 1) + else: + version = 1 + build_config = get_configuration_class(version)( + env_config, + config, + source_file=filename, + ) - project_config = ProjectConfig(build_configs) - project_config.validate() - return project_config + build_config.validate() + return build_config def get_configuration_class(version): diff --git a/readthedocs/config/parser.py b/readthedocs/config/parser.py index 7428da22055..655b1601bf7 100644 --- a/readthedocs/config/parser.py +++ b/readthedocs/config/parser.py @@ -17,18 +17,17 @@ class ParseError(Exception): def parse(stream): """ - Take file-like object and return a list of project configurations. + Take file-like object and return a project configuration. - The files need be valid YAML and only contain mappings as documents. + The file need be valid YAML and only contain mappings as document. Everything else raises a ``ParseError``. """ try: - configs = list(yaml.safe_load_all(stream)) + config = yaml.safe_load(stream) except yaml.YAMLError as error: raise ParseError('YAML: {message}'.format(message=error)) - if not configs: + if not isinstance(config, dict): + raise ParseError('Expected mapping') + if not config: raise ParseError('Empty config') - for config in configs: - if not isinstance(config, dict): - raise ParseError('Expected mapping') - return configs + return config diff --git a/readthedocs/config/tests/test_config.py b/readthedocs/config/tests/test_config.py index a95c36cbc4e..f623881a615 100644 --- a/readthedocs/config/tests/test_config.py +++ b/readthedocs/config/tests/test_config.py @@ -17,7 +17,6 @@ ConfigError, ConfigOptionNotSupportedError, InvalidConfig, - ProjectConfig, load, ) from readthedocs.config.config import ( @@ -81,13 +80,11 @@ } -def get_build_config(config, env_config=None, source_file='readthedocs.yml', - source_position=0): +def get_build_config(config, env_config=None, source_file='readthedocs.yml'): return BuildConfigV1( env_config or {}, config, source_file=source_file, - source_position=source_position, ) @@ -130,10 +127,7 @@ def test_load_empty_config_file(tmpdir): def test_minimal_config(tmpdir): apply_fs(tmpdir, minimal_config_dir) base = str(tmpdir) - config = load(base, env_config) - assert isinstance(config, ProjectConfig) - assert len(config) == 1 - build = config[0] + build = load(base, env_config) assert isinstance(build, BuildConfigV1) @@ -144,10 +138,7 @@ def test_load_version1(tmpdir): ''') }) base = str(tmpdir) - config = load(base, get_env_config({'allow_v2': True})) - assert isinstance(config, ProjectConfig) - assert len(config) == 1 - build = config[0] + build = load(base, get_env_config({'allow_v2': True})) assert isinstance(build, BuildConfigV1) @@ -158,10 +149,7 @@ def test_load_version2(tmpdir): ''') }) base = str(tmpdir) - config = load(base, get_env_config({'allow_v2': True})) - assert isinstance(config, ProjectConfig) - assert len(config) == 1 - build = config[0] + build = load(base, get_env_config({'allow_v2': True})) assert isinstance(build, BuildConfigV2) @@ -182,31 +170,18 @@ def test_yaml_extension(tmpdir): apply_fs(tmpdir, yaml_extension_config_dir) base = str(tmpdir) config = load(base, env_config) - assert len(config) == 1 + assert isinstance(config, BuildConfigV1) def test_build_config_has_source_file(tmpdir): base = str(apply_fs(tmpdir, minimal_config_dir)) - build = load(base, env_config)[0] + build = load(base, env_config) assert build.source_file == os.path.join(base, 'readthedocs.yml') - assert build.source_position == 0 - - -def test_build_config_has_source_position(tmpdir): - base = str(apply_fs(tmpdir, multiple_config_dir)) - builds = load(base, env_config) - assert len(builds) == 2 - first, second = filter( - lambda b: not b.source_file.endswith('nested/readthedocs.yml'), - builds, - ) - assert first.source_position == 0 - assert second.source_position == 1 def test_build_config_has_list_with_single_empty_value(tmpdir): base = str(apply_fs(tmpdir, config_with_explicit_empty_list)) - build = load(base, env_config)[0] + build = load(base, env_config) assert isinstance(build, BuildConfigV1) assert build.formats == [] @@ -216,7 +191,6 @@ def test_config_requires_name(): {'output_base': ''}, {}, source_file='readthedocs.yml', - source_position=0, ) with raises(InvalidConfig) as excinfo: build.validate() @@ -229,7 +203,6 @@ def test_build_requires_valid_name(): {'output_base': ''}, {'name': 'with/slashes'}, source_file='readthedocs.yml', - source_position=0, ) with raises(InvalidConfig) as excinfo: build.validate() @@ -553,7 +526,6 @@ def test_valid_build_config(): env_config, minimal_config, source_file='readthedocs.yml', - source_position=0, ) build.validate() assert build.name == 'docs' @@ -575,7 +547,6 @@ def test_it_validates_to_abspath(self, tmpdir): get_env_config(), {'base': '../docs'}, source_file=source_file, - source_position=0, ) build.validate() assert build.base == str(tmpdir.join('docs')) @@ -596,7 +567,6 @@ def test_it_fails_if_base_is_not_a_string(self, tmpdir): get_env_config(), {'base': 1}, source_file=str(tmpdir.join('readthedocs.yml')), - source_position=0, ) with raises(InvalidConfig) as excinfo: build.validate() @@ -609,7 +579,6 @@ def test_it_fails_if_base_does_not_exist(self, tmpdir): get_env_config(), {'base': 'docs'}, source_file=str(tmpdir.join('readthedocs.yml')), - source_position=0, ) with raises(InvalidConfig) as excinfo: build.validate() @@ -625,7 +594,6 @@ def test_it_fails_if_build_is_invalid_option(self, tmpdir): get_env_config(), {'build': {'image': 3.0}}, source_file=str(tmpdir.join('readthedocs.yml')), - source_position=0, ) with raises(InvalidConfig) as excinfo: build.validate() @@ -641,7 +609,6 @@ def test_it_fails_on_python_validation(self, tmpdir): 'python': {'version': '3.3'}, }, source_file=str(tmpdir.join('readthedocs.yml')), - source_position=0, ) build.validate_build() with raises(InvalidConfig) as excinfo: @@ -658,7 +625,6 @@ def test_it_works_on_python_validation(self, tmpdir): 'python': {'version': '3.3'}, }, source_file=str(tmpdir.join('readthedocs.yml')), - source_position=0, ) build.validate_build() build.validate_python() @@ -669,7 +635,6 @@ def test_it_works(self, tmpdir): get_env_config(), {'build': {'image': 'latest'}}, source_file=str(tmpdir.join('readthedocs.yml')), - source_position=0, ) build.validate() assert build.build.image == 'readthedocs/build:latest' @@ -680,7 +645,6 @@ def test_default(self, tmpdir): get_env_config(), {}, source_file=str(tmpdir.join('readthedocs.yml')), - source_position=0, ) build.validate() assert build.build.image == 'readthedocs/build:2.0' @@ -696,7 +660,6 @@ def test_it_priorities_image_from_env_config(self, tmpdir, image): get_env_config({'defaults': defaults}), {'build': {'image': 'latest'}}, source_file=str(tmpdir.join('readthedocs.yml')), - source_position=0, ) build.validate() assert build.build.image == image @@ -786,7 +749,6 @@ def test_build_validate_calls_all_subvalidators(tmpdir): {}, {}, source_file=str(tmpdir.join('readthedocs.yml')), - source_position=0, ) with patch.multiple( BuildConfigV1, @@ -802,20 +764,6 @@ def test_build_validate_calls_all_subvalidators(tmpdir): BuildConfigV1.validate_output_base.assert_called_with() -def test_validate_project_config(): - with patch.object(BuildConfigV1, 'validate') as build_validate: - project = ProjectConfig([ - BuildConfigV1( - env_config, - minimal_config, - source_file='readthedocs.yml', - source_position=0, - ), - ]) - project.validate() - assert build_validate.call_count == 1 - - def test_load_calls_validate(tmpdir): apply_fs(tmpdir, minimal_config_dir) base = str(tmpdir) @@ -896,13 +844,12 @@ def test_as_dict(tmpdir): class TestBuildConfigV2(object): - def get_build_config(self, config, env_config=None, - source_file='readthedocs.yml', source_position=0): + def get_build_config( + self, config, env_config=None, source_file='readthedocs.yml'): return BuildConfigV2( env_config or {}, config, source_file=source_file, - source_position=source_position, ) def test_version(self): diff --git a/readthedocs/config/tests/test_parser.py b/readthedocs/config/tests/test_parser.py index afdc8dde41d..5c37c3c5cb0 100644 --- a/readthedocs/config/tests/test_parser.py +++ b/readthedocs/config/tests/test_parser.py @@ -28,36 +28,35 @@ def test_parse_bad_type(): def test_parse_single_config(): buf = StringIO(u'base: path') config = parse(buf) - assert isinstance(config, list) - assert len(config) == 1 - assert config[0]['base'] == 'path' + assert isinstance(config, dict) + assert config['base'] == 'path' def test_parse_null_value(): buf = StringIO(u'base: null') config = parse(buf) - assert config[0]['base'] is None + assert config['base'] is None def test_parse_empty_value(): buf = StringIO(u'base:') config = parse(buf) - assert config[0]['base'] is None + assert config['base'] is None def test_parse_empty_string_value(): buf = StringIO(u'base: ""') config = parse(buf) - assert config[0]['base'] == '' + assert config['base'] == '' def test_parse_empty_list(): buf = StringIO(u'base: []') config = parse(buf) - assert config[0]['base'] == [] + assert config['base'] == [] -def test_parse_multiple_configs_in_one_file(): +def test_do_not_parse_multiple_configs_in_one_file(): buf = StringIO( u''' base: path @@ -67,8 +66,5 @@ def test_parse_multiple_configs_in_one_file(): nested: works: true ''') - configs = parse(buf) - assert isinstance(configs, list) - assert len(configs) == 2 - assert configs[0]['base'] == 'path' - assert configs[1]['nested'] == {'works': True} + with raises(ParseError): + parse(buf) diff --git a/readthedocs/doc_builder/config.py b/readthedocs/doc_builder/config.py index 8620082ad3e..00a0095bcc6 100644 --- a/readthedocs/doc_builder/config.py +++ b/readthedocs/doc_builder/config.py @@ -65,7 +65,7 @@ def load_yaml_config(version): config = load_config( path=checkout_path, env_config=env_config, - )[0] + ) except InvalidConfig: # This is a subclass of ConfigError, so has to come first raise @@ -74,7 +74,6 @@ def load_yaml_config(version): env_config=env_config, raw_config={}, source_file=checkout_path, - source_position=0, ) config.validate() return config diff --git a/readthedocs/rtd_tests/tests/test_config_integration.py b/readthedocs/rtd_tests/tests/test_config_integration.py index cc49317dad8..2b2c6ceb489 100644 --- a/readthedocs/rtd_tests/tests/test_config_integration.py +++ b/readthedocs/rtd_tests/tests/test_config_integration.py @@ -13,7 +13,7 @@ from mock import MagicMock, PropertyMock, patch from readthedocs.builds.models import Version -from readthedocs.config import ALL, BuildConfigV1, InvalidConfig, ProjectConfig +from readthedocs.config import ALL, BuildConfigV1, InvalidConfig from readthedocs.config.tests.utils import apply_fs from readthedocs.doc_builder.config import load_yaml_config from readthedocs.doc_builder.environments import LocalBuildEnvironment @@ -27,9 +27,7 @@ def create_load(config=None): """ Mock out the function of the build load function. - This will create a ProjectConfig list of BuildConfigV1 objects and validate - them. The default load function iterates over files and builds up a list of - objects. Instead of mocking all of this, just mock the end result. + This will create a BuildConfigV1 object and validate it. """ if config is None: config = {} @@ -41,14 +39,11 @@ def inner(path=None, env_config=None): } if env_config is not None: env_config_defaults.update(env_config) - yaml_config = ProjectConfig([ - BuildConfigV1( - env_config_defaults, - config, - source_file='readthedocs.yml', - source_position=0, - ), - ]) + yaml_config = BuildConfigV1( + env_config_defaults, + config, + source_file='readthedocs.yml', + ) yaml_config.validate() return yaml_config