diff --git a/readthedocs/config/find.py b/readthedocs/config/find.py index e518da13f2c..cb3e5e0c56d 100644 --- a/readthedocs/config/find.py +++ b/readthedocs/config/find.py @@ -6,18 +6,11 @@ import re -def find_all(path, filename_regex): - """Find all files in ``path`` that match ``filename_regex`` regex.""" - path = os.path.abspath(path) - for root, dirs, files in os.walk(path, topdown=True): - dirs.sort() - for filename in files: - if re.match(filename_regex, filename): - yield os.path.abspath(os.path.join(root, filename)) - - def find_one(path, filename_regex): """Find the first file in ``path`` that match ``filename_regex`` regex.""" - for _path in find_all(path, filename_regex): - return _path + _path = os.path.abspath(path) + for filename in os.listdir(_path): + if re.match(filename_regex, filename): + return os.path.join(_path, filename) + return '' diff --git a/readthedocs/config/tests/test_config.py b/readthedocs/config/tests/test_config.py index e2bfc37bf46..37c938331f2 100644 --- a/readthedocs/config/tests/test_config.py +++ b/readthedocs/config/tests/test_config.py @@ -4,20 +4,42 @@ import os import re import textwrap +from collections import OrderedDict import pytest from mock import DEFAULT, patch from pytest import raises from readthedocs.config import ( - ALL, BuildConfigV1, BuildConfigV2, ConfigError, - ConfigOptionNotSupportedError, InvalidConfig, ProjectConfig, load) + ALL, + BuildConfigV1, + BuildConfigV2, + ConfigError, + ConfigOptionNotSupportedError, + InvalidConfig, + ProjectConfig, + load, +) from readthedocs.config.config import ( - CONFIG_FILENAME_REGEX, CONFIG_NOT_SUPPORTED, CONFIG_REQUIRED, NAME_INVALID, - NAME_REQUIRED, PYTHON_INVALID, VERSION_INVALID) + CONFIG_FILENAME_REGEX, + CONFIG_NOT_SUPPORTED, + CONFIG_REQUIRED, + INVALID_KEY, + NAME_INVALID, + NAME_REQUIRED, + PYTHON_INVALID, + VERSION_INVALID, +) from readthedocs.config.models import Conda from readthedocs.config.validation import ( - INVALID_BOOL, INVALID_CHOICE, INVALID_LIST, INVALID_PATH, INVALID_STRING) + INVALID_BOOL, + INVALID_CHOICE, + INVALID_LIST, + INVALID_PATH, + INVALID_STRING, + VALUE_NOT_FOUND, + ValidationError, +) from .utils import apply_fs @@ -81,21 +103,24 @@ def get_env_config(extra=None): return defaults -@pytest.mark.parametrize('files', [ - {}, - {'readthedocs.ymlmore': ''}, - {'startreadthedocs.yml': ''}, - {'noroot': {'readthedocs.ymlmore': ''}}, - {'noroot': {'startreadthedocs.yml': ''}}, - {'readthebots.yaml': ''}, +@pytest.mark.parametrize('files,nested_files', [ + ({'readthedocs.ymlmore': ''}, {'first': {'readthedocs.yml': ''}}), + ({'startreadthedocs.yml': ''}, {'second': {'confuser.txt': 'content'}}), + ({'noroot': {'readthedocs.ymlmore': ''}}, {'third': {'readthedocs.yml': 'content', 'Makefile': ''}}), + ({'noroot': {'startreadthedocs.yml': ''}}, {'fourth': {'samplefile.yaml': 'content'}}), + ({'readthebots.yaml': ''}, {'fifth': {'confuser.txt': '', 'readthedocs.yml': 'content'}}), ]) -def test_load_no_config_file(tmpdir, files): +def test_load_no_config_file(tmpdir, files, nested_files): apply_fs(tmpdir, files) base = str(tmpdir) with raises(ConfigError) as e: load(base, env_config) assert e.value.code == CONFIG_REQUIRED + apply_fs(tmpdir, nested_files) + with raises(ConfigError) as ae: + load(base, env_config) + assert ae.value.code == CONFIG_REQUIRED def test_load_empty_config_file(tmpdir): apply_fs(tmpdir, { @@ -275,7 +300,6 @@ def test_python_pip_install_default(): def describe_validate_python_extra_requirements(): - def it_defaults_to_list(): build = get_build_config({'python': {}}, get_env_config()) build.validate() @@ -309,7 +333,6 @@ def it_uses_validate_string(validate_string): def describe_validate_use_system_site_packages(): - def it_defaults_to_false(): build = get_build_config({'python': {}}, get_env_config()) build.validate() @@ -337,7 +360,6 @@ def it_uses_validate_bool(validate_bool): def describe_validate_setup_py_install(): - def it_defaults_to_false(): build = get_build_config({'python': {}}, get_env_config()) build.validate() @@ -365,7 +387,6 @@ def it_uses_validate_bool(validate_bool): def describe_validate_python_version(): - def it_defaults_to_a_valid_version(): build = get_build_config({'python': {}}, get_env_config()) build.validate() @@ -465,7 +486,6 @@ def it_respects_default_value(value): def describe_validate_formats(): - def it_defaults_to_empty(): build = get_build_config({}, get_env_config()) build.validate() @@ -545,7 +565,6 @@ def test_valid_build_config(): def describe_validate_base(): - def it_validates_to_abspath(tmpdir): apply_fs(tmpdir, {'configs': minimal_config, 'docs': {}}) with tmpdir.as_cwd(): @@ -597,7 +616,6 @@ def it_fails_if_base_does_not_exist(tmpdir): def describe_validate_build(): - def it_fails_if_build_is_invalid_option(tmpdir): apply_fs(tmpdir, minimal_config) build = BuildConfigV1( @@ -1702,3 +1720,83 @@ def test_submodules_recursive_explict_default(self): assert build.submodules.include == [] assert build.submodules.exclude == [] assert build.submodules.recursive is False + + @pytest.mark.parametrize('value,key', [ + ({'typo': 'something'}, 'typo'), + ( + { + 'pyton': { + 'version': 'another typo', + } + }, + 'pyton.version' + ), + ( + { + 'build': { + 'image': 'latest', + 'extra': 'key', + } + }, + 'build.extra' + ) + ]) + def test_strict_validation(self, value, key): + build = self.get_build_config(value) + with raises(InvalidConfig) as excinfo: + build.validate() + assert excinfo.value.key == key + assert excinfo.value.code == INVALID_KEY + + def test_strict_validation_pops_all_keys(self): + build = self.get_build_config({ + 'version': 2, + 'python': { + 'version': 3, + }, + }) + build.validate() + assert build.raw_config == {} + + @pytest.mark.parametrize('value,expected', [ + ({}, []), + ({'one': 1}, ['one']), + ({'one': {'two': 3}}, ['one', 'two']), + (OrderedDict([('one', 1), ('two', 2)]), ['one']), + (OrderedDict([('one', {'two': 2}), ('three', 3)]), ['one', 'two']), + ]) + def test_get_extra_key(self, value, expected): + build = self.get_build_config({}) + assert build._get_extra_key(value) == expected + + def test_pop_config_single(self): + build = self.get_build_config({'one': 1}) + build.pop_config('one') + assert build.raw_config == {} + + def test_pop_config_nested(self): + build = self.get_build_config({'one': {'two': 2}}) + build.pop_config('one.two') + assert build.raw_config == {} + + def test_pop_config_nested_with_residue(self): + build = self.get_build_config({'one': {'two': 2, 'three': 3}}) + build.pop_config('one.two') + assert build.raw_config == {'one': {'three': 3}} + + def test_pop_config_default_none(self): + build = self.get_build_config({'one': {'two': 2, 'three': 3}}) + assert build.pop_config('one.four') is None + assert build.raw_config == {'one': {'two': 2, 'three': 3}} + + def test_pop_config_default(self): + build = self.get_build_config({'one': {'two': 2, 'three': 3}}) + assert build.pop_config('one.four', 4) == 4 + assert build.raw_config == {'one': {'two': 2, 'three': 3}} + + def test_pop_config_raise_exception(self): + build = self.get_build_config({'one': {'two': 2, 'three': 3}}) + with raises(ValidationError) as excinfo: + build.pop_config('one.four', raise_ex=True) + assert excinfo.value.value == 'four' + assert excinfo.value.code == VALUE_NOT_FOUND diff --git a/readthedocs/config/tests/test_find.py b/readthedocs/config/tests/test_find.py index 2005d839277..104e726ec74 100644 --- a/readthedocs/config/tests/test_find.py +++ b/readthedocs/config/tests/test_find.py @@ -1,77 +1,23 @@ from __future__ import division, print_function, unicode_literals import os - import pytest import six - -from readthedocs.config.find import find_all, find_one - +from readthedocs.config.find import find_one from .utils import apply_fs def test_find_no_files(tmpdir): with tmpdir.as_cwd(): - paths = list(find_all(os.getcwd(), r'readthedocs.yml')) - assert len(paths) == 0 + path = find_one(os.getcwd(), r'readthedocs.yml') + assert path == '' def test_find_at_root(tmpdir): apply_fs(tmpdir, {'readthedocs.yml': '', 'otherfile.txt': ''}) - - base = str(tmpdir) - paths = list(find_all(base, r'readthedocs\.yml')) - assert paths == [ - os.path.abspath(os.path.join(base, 'readthedocs.yml')) - ] - - -def test_find_nested(tmpdir): - apply_fs(tmpdir, { - 'first': { - 'readthedocs.yml': '', - }, - 'second': { - 'confuser.txt': 'content', - }, - 'third': { - 'readthedocs.yml': 'content', - 'Makefile': '', - }, - }) - apply_fs(tmpdir, {'first/readthedocs.yml': ''}) - - base = str(tmpdir) - paths = set(find_all(base, r'readthedocs\.yml')) - assert paths == { - str(tmpdir.join('first', 'readthedocs.yml')), - str(tmpdir.join('third', 'readthedocs.yml')), - } - - -def test_find_multiple_files(tmpdir): - apply_fs(tmpdir, { - 'first': { - 'readthedocs.yml': '', - '.readthedocs.yml': 'content', - }, - 'second': { - 'confuser.txt': 'content', - }, - 'third': { - 'readthedocs.yml': 'content', - 'Makefile': '', - }, - }) - apply_fs(tmpdir, {'first/readthedocs.yml': ''}) - base = str(tmpdir) - paths = set(find_all(base, r'\.?readthedocs\.yml')) - assert paths == { - str(tmpdir.join('first', 'readthedocs.yml')), - str(tmpdir.join('first', '.readthedocs.yml')), - str(tmpdir.join('third', 'readthedocs.yml')), - } + path = find_one(base, r'readthedocs\.yml') + assert path == os.path.abspath(os.path.join(base, 'readthedocs.yml')) @pytest.mark.skipif(not six.PY2, reason='Only for python2')