diff --git a/docs/config-file/v2.rst b/docs/config-file/v2.rst index 2de0411ae94..040a8e57bc1 100644 --- a/docs/config-file/v2.rst +++ b/docs/config-file/v2.rst @@ -31,7 +31,7 @@ Below is an example YAML file which shows the most common configuration options: # Optionally set the version of Python and requirements required to build your docs python: - version: 3.7 + version: "3.7" install: - requirements: docs/requirements.txt @@ -51,7 +51,7 @@ Below is an example YAML file which shows the most common configuration options: # Optionally set the version of Python and requirements required to build your docs python: - version: 3.7 + version: "3.7" install: - requirements: docs/requirements.txt @@ -138,7 +138,7 @@ Configuration of the Python environment to be used. version: 2 python: - version: 3.7 + version: "3.7" install: - requirements: docs/requirements.txt - method: pip @@ -154,13 +154,18 @@ python.version The Python version (this depends on :ref:`config-file/v2:build.image`). -:Type: ``number`` +:Type: ``string`` :Default: ``3`` +.. note:: + + Make sure to use quotes (``"``) to make it a string. + We previously supported using numbers here, + but that approach is deprecated. .. warning:: - If you are using a :ref:`Conda ` environment to manage - the build, this setting will not have any effect, as the Python version is managed by Conda. + If you are using a :ref:`Conda ` environment to manage + the build, this setting will not have any effect, as the Python version is managed by Conda. python.install `````````````` @@ -189,7 +194,7 @@ Example: version: 2 python: - version: 3.7 + version: "3.7" install: - requirements: docs/requirements.txt - requirements: requirements.txt @@ -237,7 +242,7 @@ Example: version: 2 python: - version: 3.7 + version: "3.7" install: - method: pip path: . @@ -308,7 +313,7 @@ Configuration for the documentation build process. - cmake python: - version: 3.7 + version: "3.7" build.image diff --git a/readthedocs/config/config.py b/readthedocs/config/config.py index c1a3c4c8029..0a9f37c2003 100644 --- a/readthedocs/config/config.py +++ b/readthedocs/config/config.py @@ -254,23 +254,22 @@ def validate(self): @property def python_interpreter(self): - ver = self.python_full_version - if not ver or isinstance(ver, (int, float)): - return 'python{}'.format(ver) - - # Allow to specify ``pypy3.5`` as Python interpreter - return ver + version = self.python_full_version + if version.startswith('pypy'): + # Allow to specify ``pypy3.5`` as Python interpreter + return version + return f'python{version}' @property def python_full_version(self): - ver = self.python.version - if ver in [2, 3]: + version = self.python.version + if version in ['2', '3']: # use default Python version if user only set '2', or '3' return self.get_default_python_version_for_image( self.build.image, - ver, + version, ) - return ver + return version @property def valid_build_images(self): @@ -454,7 +453,7 @@ def validate_python(self): """Validates the ``python`` key, set default values it's necessary.""" install_project = self.defaults.get('install_project', False) use_system_packages = self.defaults.get('use_system_packages', False) - version = self.defaults.get('python_version', 2) + version = self.defaults.get('python_version', '2') python = { 'use_system_site_packages': use_system_packages, 'install_with_pip': False, @@ -513,17 +512,7 @@ def validate_python(self): if 'version' in raw_python: with self.catch_validation_error('python.version'): - # Try to convert strings to an int first, to catch '2', then - # a float, to catch '2.7' - version = raw_python['version'] - if isinstance(version, str): - try: - version = int(version) - except ValueError: - try: - version = float(version) - except ValueError: - pass + version = str(raw_python['version']) python['version'] = validate_choice( version, self.get_valid_python_versions(), @@ -839,15 +828,13 @@ def validate_python(self): python = {} with self.catch_validation_error('python.version'): - version = self.pop_config('python.version', 3) - if isinstance(version, str): - try: - version = int(version) - except ValueError: - try: - version = float(version) - except ValueError: - pass + version = self.pop_config('python.version', '3') + if version == 3.1: + # Special case for ``python.version: 3.10``, + # yaml will transform this to the numeric value of `3.1`. + # Save some frustration to users. + version = '3.10' + version = str(version) python['version'] = validate_choice( version, self.get_valid_python_versions(), diff --git a/readthedocs/config/tests/test_config.py b/readthedocs/config/tests/test_config.py index c4bbe6dc069..56d832befd6 100644 --- a/readthedocs/config/tests/test_config.py +++ b/readthedocs/config/tests/test_config.py @@ -341,18 +341,18 @@ class TestValidatePythonVersion: def test_it_defaults_to_a_valid_version(self): build = get_build_config({'python': {}}) build.validate() - assert build.python.version == 2 + assert build.python.version == '2' assert build.python_interpreter == 'python2.7' - assert build.python_full_version == 2.7 + assert build.python_full_version == '2.7' def test_it_supports_other_versions(self): build = get_build_config( {'python': {'version': 3.7}}, ) build.validate() - assert build.python.version == 3.7 + assert build.python.version == '3.7' assert build.python_interpreter == 'python3.7' - assert build.python_full_version == 3.7 + assert build.python_full_version == '3.7' def test_it_supports_string_versions(self): build = get_build_config( @@ -381,28 +381,11 @@ def test_it_validates_wrong_type(self): assert excinfo.value.key == 'python.version' assert excinfo.value.code == INVALID_CHOICE - def test_it_validates_wrong_type_right_value(self): - build = get_build_config( - {'python': {'version': '3.6'}}, - ) - build.validate() - assert build.python.version == 3.6 - assert build.python_interpreter == 'python3.6' - assert build.python_full_version == 3.6 - - build = get_build_config( - {'python': {'version': '3'}}, - ) - build.validate() - assert build.python.version == 3 - assert build.python_interpreter == 'python3.7' - assert build.python_full_version == 3.7 - def test_it_validates_env_supported_versions(self): build = get_build_config( - {'python': {'version': 3.6}}, + {'python': {'version': '3.6'}}, env_config={ - 'python': {'supported_versions': [3.5]}, + 'python': {'supported_versions': ['3.5']}, 'build': {'image': 'custom'}, }, ) @@ -412,18 +395,18 @@ def test_it_validates_env_supported_versions(self): assert excinfo.value.code == INVALID_CHOICE build = get_build_config( - {'python': {'version': 3.6}}, + {'python': {'version': '3.6'}}, env_config={ - 'python': {'supported_versions': [3.5, 3.6]}, + 'python': {'supported_versions': ['3.5', '3.6']}, 'build': {'image': 'custom'}, }, ) build.validate() - assert build.python.version == 3.6 + assert build.python.version == '3.6' assert build.python_interpreter == 'python3.6' - assert build.python_full_version == 3.6 + assert build.python_full_version == '3.6' - @pytest.mark.parametrize('value', [2, 3]) + @pytest.mark.parametrize('value', ['2', '3']) def test_it_respects_default_value(self, value): defaults = { 'python_version': value, @@ -741,7 +724,7 @@ def test_as_dict(tmpdir): 'version': '1', 'formats': ['pdf'], 'python': { - 'version': 3.7, + 'version': '3.7', 'install': [{ 'requirements': 'requirements.txt', }], @@ -1003,8 +986,8 @@ def test_python_check_invalid_types(self, value): @pytest.mark.parametrize( 'image,versions', [ - ('latest', [2, 2.7, 3, 3.5, 3.6, 3.7, 'pypy3.5']), - ('stable', [2, 2.7, 3, 3.5, 3.6, 3.7]), + ('latest', ['2', '2.7', '3', '3.5', '3.6', '3.7', 'pypy3.5']), + ('stable', ['2', '2.7', '3', '3.5', '3.6', '3.7']), ], ) def test_python_version(self, image, versions): @@ -1030,7 +1013,31 @@ def test_python_version_accepts_string(self): }, }) build.validate() - assert build.python.version == 3.6 + assert build.python.version == '3.6' + + def test_python_version_accepts_number(self): + build = self.get_build_config({ + 'build': { + 'image': 'latest', + }, + 'python': { + 'version': 3.6, + }, + }) + build.validate() + assert build.python.version == '3.6' + + def test_python_version_310_as_number(self): + build = self.get_build_config({ + 'build': { + 'image': 'testing', + }, + 'python': { + 'version': 3.10, + }, + }) + build.validate() + assert build.python.version == '3.10' @pytest.mark.parametrize( 'image,versions', @@ -1056,27 +1063,27 @@ def test_python_version_invalid(self, image, versions): def test_python_version_default(self): build = self.get_build_config({}) build.validate() - assert build.python.version == 3 + assert build.python.version == '3' @pytest.mark.parametrize( - 'image,default_version', + 'image, default_version, full_version', [ - ('2.0', 3.5), - ('4.0', 3.7), - ('5.0', 3.7), - ('latest', 3.7), - ('stable', 3.7), + ('2.0', '3', '3.5'), + ('4.0', '3', '3.7'), + ('5.0', '3', '3.7'), + ('latest', '3', '3.7'), + ('stable', '3', '3.7'), ], ) - def test_python_version_default_from_image(self, image, default_version): + def test_python_version_default_from_image(self, image, default_version, full_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 + assert build.python.version == default_version + assert build.python_full_version == full_version @pytest.mark.parametrize('value', [2, 3]) def test_python_version_overrides_default(self, value): @@ -1085,13 +1092,13 @@ def test_python_version_overrides_default(self, value): {'defaults': {'python_version': value}}, ) build.validate() - assert build.python.version == 3 + assert build.python.version == '3' - @pytest.mark.parametrize('value', [2, 3, 3.6]) + @pytest.mark.parametrize('value', ['2', '3', '3.6']) def test_python_version_priority_over_default(self, value): build = self.get_build_config( {'python': {'version': value}}, - {'defaults': {'python_version': 3}}, + {'defaults': {'python_version': '3'}}, ) build.validate() assert build.python.version == value @@ -2109,7 +2116,7 @@ def test_as_dict(self, tmpdir): 'version': 2, 'formats': ['pdf'], 'python': { - 'version': 3.6, + 'version': '3.6', 'install': [{ 'requirements': 'requirements.txt', }], @@ -2122,7 +2129,7 @@ def test_as_dict(self, tmpdir): 'version': '2', 'formats': ['pdf'], 'python': { - 'version': 3.6, + 'version': '3.6', 'install': [{ 'requirements': 'requirements.txt', }], diff --git a/readthedocs/doc_builder/config.py b/readthedocs/doc_builder/config.py index 5f57a4402b1..02e7818578d 100644 --- a/readthedocs/doc_builder/config.py +++ b/readthedocs/doc_builder/config.py @@ -24,7 +24,7 @@ def load_yaml_config(version): # can be rejected at validation img_name = project.container_image or DOCKER_IMAGE - python_version = 3 if project.python_interpreter == 'python3' else 2 + python_version = '3' if project.python_interpreter == 'python3' else '2' try: sphinx_configuration = path.join( version.get_conf_py_path(), diff --git a/readthedocs/rtd_tests/tests/test_config_integration.py b/readthedocs/rtd_tests/tests/test_config_integration.py index acd65232826..f8928be5526 100644 --- a/readthedocs/rtd_tests/tests/test_config_integration.py +++ b/readthedocs/rtd_tests/tests/test_config_integration.py @@ -99,7 +99,7 @@ def test_python_supported_versions_default_image_1_0(self, load_config): ], 'use_system_packages': self.project.use_system_packages, 'requirements_file': self.project.requirements_file, - 'python_version': 3, + 'python_version': '3', 'sphinx_configuration': mock.ANY, 'build_image': 'readthedocs/build:1.0', 'doctype': self.project.documentation_type, @@ -114,7 +114,7 @@ def test_python_supported_versions_default_image_1_0(self, load_config): path=mock.ANY, env_config=expected_env_config, ) - self.assertEqual(config.python.version, 3) + self.assertEqual(config.python.version, '3') @mock.patch('readthedocs.doc_builder.config.load_config') def test_python_supported_versions_image_2_0(self, load_config): @@ -124,7 +124,7 @@ def test_python_supported_versions_image_2_0(self, load_config): config = load_yaml_config(self.version) self.assertEqual( config.get_valid_python_versions(), - [2, 2.7, 3, 3.5], + ['2', '2.7', '3', '3.5'], ) @mock.patch('readthedocs.doc_builder.config.load_config') @@ -135,14 +135,14 @@ def test_python_supported_versions_image_latest(self, load_config): config = load_yaml_config(self.version) self.assertEqual( config.get_valid_python_versions(), - [2, 2.7, 3, 3.5, 3.6, 3.7, 3.8, 'pypy3.5'], + ['2', '2.7', '3', '3.5', '3.6', '3.7', '3.8', 'pypy3.5'], ) @mock.patch('readthedocs.doc_builder.config.load_config') def test_python_default_version(self, load_config): load_config.side_effect = create_load() config = load_yaml_config(self.version) - self.assertEqual(config.python.version, 3) + self.assertEqual(config.python.version, '3') self.assertEqual(config.python_interpreter, 'python3.7') @mock.patch('readthedocs.doc_builder.config.load_config') @@ -152,7 +152,7 @@ def test_python_set_python_version_on_project(self, load_config): self.project.python_interpreter = 'python3' self.project.save() config = load_yaml_config(self.version) - self.assertEqual(config.python.version, 3) + self.assertEqual(config.python.version, '3') self.assertEqual(config.python_interpreter, 'python3.5') @mock.patch('readthedocs.doc_builder.config.load_config') @@ -163,9 +163,19 @@ def test_python_set_python_version_in_config(self, load_config): self.project.container_image = 'readthedocs/build:2.0' self.project.save() config = load_yaml_config(self.version) - self.assertEqual(config.python.version, 3.5) + self.assertEqual(config.python.version, '3.5') self.assertEqual(config.python_interpreter, 'python3.5') + @mock.patch('readthedocs.doc_builder.config.load_config') + def test_python_set_python_310_version_in_config(self, load_config): + load_config.side_effect = create_load({ + 'build': {'image': 'testing'}, + 'python': {'version': '3.10'}, + }) + config = load_yaml_config(self.version) + self.assertEqual(config.python.version, '3.10') + self.assertEqual(config.python_interpreter, 'python3.10') + @mock.patch('readthedocs.doc_builder.config.load_config') def test_python_invalid_version_in_config(self, load_config): load_config.side_effect = create_load({ @@ -523,8 +533,8 @@ def test_python_version(self, checkout_path, tmpdir): self.project.save() config = self.get_update_docs_task().config - assert config.python.version == 3 - assert config.python_full_version == 3.7 + assert config.python.version == '3' + assert config.python_full_version == '3.7' @patch('readthedocs.doc_builder.environments.BuildEnvironment.run') def test_python_install_requirements(self, run, checkout_path, tmpdir): diff --git a/readthedocs/rtd_tests/tests/test_doc_building.py b/readthedocs/rtd_tests/tests/test_doc_building.py index 82cfa210a76..f0b8599ad6c 100644 --- a/readthedocs/rtd_tests/tests/test_doc_building.py +++ b/readthedocs/rtd_tests/tests/test_doc_building.py @@ -1501,7 +1501,7 @@ def test_save_environment_json(self, load_config): 'hash': 'a1b2c3', }, 'python': { - 'version': 2.7, + 'version': '2.7', }, 'env_vars_hash': env_vars_hash } @@ -1638,7 +1638,7 @@ def test_is_obsolete_with_json_same_data_as_version(self, load_config): m.update(env_var_str.encode('utf-8')) env_vars_hash = m.hexdigest() - env_json_data = '{{"build": {{"image": "readthedocs/build:2.0", "hash": "a1b2c3"}}, "python": {{"version": 3.5}}, "env_vars_hash": "{}"}}'.format(env_vars_hash) # noqa + env_json_data = '{{"build": {{"image": "readthedocs/build:2.0", "hash": "a1b2c3"}}, "python": {{"version": "3.5"}}, "env_vars_hash": "{}"}}'.format(env_vars_hash) # noqa with patch('os.path.exists') as exists, patch('readthedocs.doc_builder.python_environments.open', mock_open(read_data=env_json_data)) as _open: # noqa exists.return_value = True self.assertFalse(python_env.is_obsolete) diff --git a/readthedocs/settings/base.py b/readthedocs/settings/base.py index 4b76c941c5a..e30011cecb2 100644 --- a/readthedocs/settings/base.py +++ b/readthedocs/settings/base.py @@ -455,50 +455,51 @@ def TEMPLATES(self): # We must have documented it at some point. 'readthedocs/build:2.0': { 'python': { - 'supported_versions': [2, 2.7, 3, 3.5], + 'supported_versions': ['2', '2.7', '3', '3.5'], 'default_version': { - 2: 2.7, - 3: 3.5, + '2': '2.7', + '3': '3.5', }, }, }, 'readthedocs/build:4.0': { 'python': { - 'supported_versions': [2, 2.7, 3, 3.5, 3.6, 3.7], + 'supported_versions': ['2', '2.7', '3', '3.5', '3.6', 3.7], 'default_version': { - 2: 2.7, - 3: 3.7, + '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'], + 'supported_versions': ['2', '2.7', '3', '3.5', '3.6', '3.7', 'pypy3.5'], 'default_version': { - 2: 2.7, - 3: 3.7, + '2': '2.7', + '3': '3.7', }, }, }, 'readthedocs/build:6.0': { 'python': { - 'supported_versions': [2, 2.7, 3, 3.5, 3.6, 3.7, 3.8, 'pypy3.5'], + 'supported_versions': ['2', '2.7', '3', '3.5', '3.6', '3.7', '3.8', 'pypy3.5'], 'default_version': { - 2: 2.7, - 3: 3.7, + '2': '2.7', + '3': '3.7', }, }, }, 'readthedocs/build:7.0': { 'python': { - 'supported_versions': [2, 2.7, 3, 3.5, 3.6, 3.7, 3.8, 3.9, 'pypy3.5'], + 'supported_versions': ['2', '2.7', '3', '3.5', '3.6', '3.7', '3.8', '3.9', '3.10', 'pypy3.5'], 'default_version': { - 2: 2.7, - 3: 3.7, + '2': '2.7', + '3': '3.7', }, }, }, } + # Alias tagged via ``docker tag`` on the build servers DOCKER_IMAGE_SETTINGS.update({ 'readthedocs/build:stable': DOCKER_IMAGE_SETTINGS.get('readthedocs/build:5.0'),