diff --git a/readthedocs/config/config.py b/readthedocs/config/config.py index 5dcc89f6d80..38f30fa5174 100644 --- a/readthedocs/config/config.py +++ b/readthedocs/config/config.py @@ -30,12 +30,11 @@ validate_bool, validate_choice, validate_dict, - validate_path, validate_list, + validate_path, validate_string, ) - __all__ = ( 'ALL', 'load', @@ -83,7 +82,7 @@ class ConfigFileNotFound(ConfigError): def __init__(self, directory): super().__init__( - f"Configuration file not found in: {directory}", + f'Configuration file not found in: {directory}', CONFIG_FILE_REQUIRED, ) @@ -255,11 +254,10 @@ def python_interpreter(self): def python_full_version(self): ver = self.python.version if ver in [2, 3]: - # Get the highest version of the major series version if user only - # gave us a version of '2', or '3' - ver = max( - v for v in self.get_valid_python_versions() - if not isinstance(v, str) and v < ver + 1 + # use default Python version if user only set '2', or '3' + return self.get_default_python_version_for_image( + self.build.image, + ver, ) return ver @@ -296,6 +294,32 @@ def get_valid_python_versions_for_image(self, build_image): ) return settings.DOCKER_IMAGE_SETTINGS[build_image]['python']['supported_versions'] + def get_default_python_version_for_image(self, build_image, python_version): + """ + Return the default Python version for Docker image and Py2 or Py3. + + :param build_image: the Docker image complete name, already validated + (``readthedocs/build:4.0``, not just ``4.0``) + :type build_image: str + + :param python_version: major Python version (``2`` or ``3``) to get its + default full version + :type python_version: int + + :returns: default version for the ``DOCKER_DEFAULT_VERSION`` if not + ``build_image`` found. + """ + if build_image not in settings.DOCKER_IMAGE_SETTINGS: + build_image = '{}:{}'.format( + settings.DOCKER_DEFAULT_IMAGE, + self.default_build_image, + ) + return ( + # For linting + settings.DOCKER_IMAGE_SETTINGS[build_image]['python'] + ['default_version'][python_version] + ) + def as_dict(self): config = {} for name in self.PUBLIC_ATTRIBUTES: @@ -405,7 +429,9 @@ def validate_build(self): ) # Update docker default settings from image name if build['image'] in settings.DOCKER_IMAGE_SETTINGS: - self.env_config.update(settings.DOCKER_IMAGE_SETTINGS[build['image']]) + self.env_config.update( + settings.DOCKER_IMAGE_SETTINGS[build['image']] + ) # Allow to override specific project config_image = self.defaults.get('build_image') diff --git a/readthedocs/config/tests/test_config.py b/readthedocs/config/tests/test_config.py index c6b32626281..f756e5a80b1 100644 --- a/readthedocs/config/tests/test_config.py +++ b/readthedocs/config/tests/test_config.py @@ -997,6 +997,28 @@ def test_python_version_default(self): build.validate() assert build.python.version == 3 + @pytest.mark.parametrize( + 'image,default_version', + [ + ('1.0', 3.4), + ('2.0', 3.5), + ('3.0', 3.6), + ('4.0', 3.7), + ('5.0', 3.7), + ('latest', 3.7), + ('stable', 3.7), + ], + ) + def test_python_version_default_from_image(self, image, default_version): + build = self.get_build_config({ + 'build': { + 'image': image, + }, + }) + build.validate() + assert build.python.version == int(default_version) # 2 or 3 + assert build.python_full_version == default_version + @pytest.mark.parametrize('value', [2, 3]) def test_python_version_overrides_default(self, value): build = self.get_build_config( diff --git a/readthedocs/settings/base.py b/readthedocs/settings/base.py index 49d1bb3f9e5..1342af4a4f3 100644 --- a/readthedocs/settings/base.py +++ b/readthedocs/settings/base.py @@ -371,22 +371,58 @@ def USE_PROMOS(self): # noqa DOCKER_IMAGE = '{}:{}'.format(DOCKER_DEFAULT_IMAGE, DOCKER_DEFAULT_VERSION) DOCKER_IMAGE_SETTINGS = { 'readthedocs/build:1.0': { - 'python': {'supported_versions': [2, 2.7, 3, 3.4]}, + 'python': { + 'supported_versions': [2, 2.7, 3, 3.4], + 'default_version': { + 2: 2.7, + 3: 3.4, + }, + }, }, 'readthedocs/build:2.0': { - 'python': {'supported_versions': [2, 2.7, 3, 3.5]}, + 'python': { + 'supported_versions': [2, 2.7, 3, 3.5], + 'default_version': { + 2: 2.7, + 3: 3.5, + }, + }, }, 'readthedocs/build:3.0': { - 'python': {'supported_versions': [2, 2.7, 3, 3.3, 3.4, 3.5, 3.6]}, + 'python': { + 'supported_versions': [2, 2.7, 3, 3.3, 3.4, 3.5, 3.6], + 'default_version': { + 2: 2.7, + 3: 3.6, + }, + }, }, 'readthedocs/build:4.0': { - 'python': {'supported_versions': [2, 2.7, 3, 3.5, 3.6, 3.7]}, + 'python': { + 'supported_versions': [2, 2.7, 3, 3.5, 3.6, 3.7], + 'default_version': { + 2: 2.7, + 3: 3.7, + }, + }, }, 'readthedocs/build:5.0': { - 'python': {'supported_versions': [2, 2.7, 3, 3.5, 3.6, 3.7, 'pypy3.5']}, + 'python': { + 'supported_versions': [2, 2.7, 3, 3.5, 3.6, 3.7, 'pypy3.5'], + 'default_version': { + 2: 2.7, + 3: 3.7, + }, + }, }, 'readthedocs/build:6.0rc1': { - 'python': {'supported_versions': [2, 2.7, 3, 3.5, 3.6, 3.7, 3.8, 'pypy3.5']}, + 'python': { + 'supported_versions': [2, 2.7, 3, 3.5, 3.6, 3.7, 3.8, 'pypy3.5'], + 'default_version': { + 2: 2.7, + 3: 3.7, + }, + }, }, }