From d4d355ca2d69057423ac652e9873839886000c30 Mon Sep 17 00:00:00 2001 From: Manuel Kaufmann Date: Thu, 21 Dec 2017 13:50:53 -0500 Subject: [PATCH 01/12] Check versions used to create the venv and auto-wipe --- .../doc_builder/python_environments.py | 61 ++++++++++++++++++- readthedocs/projects/tasks.py | 9 ++- 2 files changed, 66 insertions(+), 4 deletions(-) diff --git a/readthedocs/doc_builder/python_environments.py b/readthedocs/doc_builder/python_environments.py index 74879ac4ce7..94959b8b6c6 100644 --- a/readthedocs/doc_builder/python_environments.py +++ b/readthedocs/doc_builder/python_environments.py @@ -1,12 +1,17 @@ +# -*- coding: utf-8 -*- """An abstraction over virtualenv and Conda environments.""" -from __future__ import absolute_import -from builtins import object +from __future__ import ( + absolute_import, division, print_function, unicode_literals) + +import json import logging import os import shutil +from builtins import object from django.conf import settings +from packaging.version import Version from readthedocs.doc_builder.config import ConfigWrapper from readthedocs.doc_builder.loader import get_builder_class @@ -38,7 +43,6 @@ def _log(self, msg): msg=msg)) def delete_existing_build_dir(self): - # Handle deleting old build dir build_dir = os.path.join( self.venv_path(), @@ -47,6 +51,13 @@ def delete_existing_build_dir(self): self._log('Removing existing build directory') shutil.rmtree(build_dir) + def delete_existing_env_dir(self): + venv_dir = self.venv_path() + # Handle deleting old venv dir + if os.path.exists(venv_dir): + self._log('Removing existing venv directory') + shutil.rmtree(venv_dir) + def install_package(self): if self.config.install_project: if self.config.pip_install or getattr(settings, 'USE_PIP_INSTALL', False): @@ -96,6 +107,50 @@ class Virtualenv(PythonEnvironment): .. _virtualenv: https://virtualenv.pypa.io/ """ + @property + def is_obsolete(self): + """ + Determine if the Python version of the venv obsolete. + + It checks the the data stored at ``environment.json`` and compares it + against the Python version in the project version to be built and the + Docker image used to create the venv against the one in the project + version config. + + :returns: ``True`` when it's obsolete and ``False`` otherwise + + :rtype: bool + """ + # Always return True if we don't have information about what Python + # version/Docker image was used to create the venv as backward + # compatibility. + if not os.path.exists(self.environment_json_path()): + return False + + try: + environment_conf = json.load(self.environment_json_path()) + env_python_version = Version(environment_conf['python']['version']) + env_build_image = Version(environment_conf['build']['image']) + except (TypeError, KeyError, json.decoder.JSONDecodeError): + return False + + # If the user define the Python version just as a major version + # (e.g. ``2`` or ``3``) we won't know exactly which exact version was + # used to create the venv but we can still compare it against the new + # one coming from the project version config. + return any([ + env_python_version != Version(self.config.python_version), + env_build_image != self.config.build_image, + ]) + + def environment_json_path(self): + return os.path.join( + self.project.doc_path, + 'envs', + self.version.slug, + 'environment.json', + ) + def venv_path(self): return os.path.join(self.project.doc_path, 'envs', self.version.slug) diff --git a/readthedocs/projects/tasks.py b/readthedocs/projects/tasks.py index 0bf82c85500..5f077f25a28 100644 --- a/readthedocs/projects/tasks.py +++ b/readthedocs/projects/tasks.py @@ -435,7 +435,14 @@ def setup_environment(self): version=self.version, max_lock_age=getattr(settings, 'REPO_LOCK_SECONDS', 30)): - self.python_env.delete_existing_build_dir() + # Check if the python version/build image in the current venv is the + # same to be used in this build and if it differs, wipe the venv to + # avoid conflicts. + if self.python_env.is_obsolete: + self.python_env.delete_existing_venv_dir() + else: + self.python_env.delete_existing_build_dir() + self.python_env.setup_base() self.python_env.install_core_requirements() self.python_env.install_user_requirements() From 1541bf0683c8dfdea050e36b1808095462818948 Mon Sep 17 00:00:00 2001 From: Manuel Kaufmann Date: Thu, 21 Dec 2017 15:20:18 -0500 Subject: [PATCH 02/12] Save environment.json file --- .../doc_builder/python_environments.py | 47 +++++++++++++------ readthedocs/projects/tasks.py | 1 + 2 files changed, 33 insertions(+), 15 deletions(-) diff --git a/readthedocs/doc_builder/python_environments.py b/readthedocs/doc_builder/python_environments.py index 94959b8b6c6..03a11df88fb 100644 --- a/readthedocs/doc_builder/python_environments.py +++ b/readthedocs/doc_builder/python_environments.py @@ -98,14 +98,14 @@ def venv_bin(self, filename=None): parts.append(filename) return os.path.join(*parts) - -class Virtualenv(PythonEnvironment): - - """ - A virtualenv_ environment. - - .. _virtualenv: https://virtualenv.pypa.io/ - """ + def environment_json_path(self): + """ + Return the path to the ``environment.json`` file for this venv. + """ + return os.path.join( + self.venv_path(), + 'environment.json', + ) @property def is_obsolete(self): @@ -143,13 +143,30 @@ def is_obsolete(self): env_build_image != self.config.build_image, ]) - def environment_json_path(self): - return os.path.join( - self.project.doc_path, - 'envs', - self.version.slug, - 'environment.json', - ) + def save_environment_json(self): + """ + Save on disk Python and build image versions used to create the venv. + """ + data = { + 'python': { + 'version': self.config.python_version, + }, + 'build': { + 'image': self.config.build_image, + }, + } + with open(self.environment_json_path(), 'w') as fpath: + json.dump(data, fpath) + + + +class Virtualenv(PythonEnvironment): + + """ + A virtualenv_ environment. + + .. _virtualenv: https://virtualenv.pypa.io/ + """ def venv_path(self): return os.path.join(self.project.doc_path, 'envs', self.version.slug) diff --git a/readthedocs/projects/tasks.py b/readthedocs/projects/tasks.py index 5f077f25a28..a376e30ff38 100644 --- a/readthedocs/projects/tasks.py +++ b/readthedocs/projects/tasks.py @@ -444,6 +444,7 @@ def setup_environment(self): self.python_env.delete_existing_build_dir() self.python_env.setup_base() + self.python_env.save_environment_json() self.python_env.install_core_requirements() self.python_env.install_user_requirements() self.python_env.install_package() From 85d76034ba5d5190ed308c3b9217bb8c39efd50c Mon Sep 17 00:00:00 2001 From: Manuel Kaufmann Date: Thu, 21 Dec 2017 17:07:06 -0500 Subject: [PATCH 03/12] Open file properly --- readthedocs/doc_builder/python_environments.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/readthedocs/doc_builder/python_environments.py b/readthedocs/doc_builder/python_environments.py index 03a11df88fb..45ef8f85b82 100644 --- a/readthedocs/doc_builder/python_environments.py +++ b/readthedocs/doc_builder/python_environments.py @@ -128,10 +128,11 @@ def is_obsolete(self): return False try: - environment_conf = json.load(self.environment_json_path()) + with open(self.environment_json_path(), 'r') as fpath: + environment_conf = json.load(fpath) env_python_version = Version(environment_conf['python']['version']) env_build_image = Version(environment_conf['build']['image']) - except (TypeError, KeyError, json.decoder.JSONDecodeError): + except (TypeError, KeyError, ValueError): return False # If the user define the Python version just as a major version From 05c2aed300cdae0578c34e2cfc67a784e898ca7e Mon Sep 17 00:00:00 2001 From: Manuel Kaufmann Date: Thu, 21 Dec 2017 17:07:26 -0500 Subject: [PATCH 04/12] Make it compatible with current master --- readthedocs/doc_builder/python_environments.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/readthedocs/doc_builder/python_environments.py b/readthedocs/doc_builder/python_environments.py index 45ef8f85b82..6fc5ce1101e 100644 --- a/readthedocs/doc_builder/python_environments.py +++ b/readthedocs/doc_builder/python_environments.py @@ -141,7 +141,8 @@ def is_obsolete(self): # one coming from the project version config. return any([ env_python_version != Version(self.config.python_version), - env_build_image != self.config.build_image, + # TODO: remove getattr when https://github.com/rtfd/readthedocs.org/pull/3339 got merged + env_build_image != getattr(self.config, 'build_image', self.version.project.container_image), ]) def save_environment_json(self): @@ -153,7 +154,8 @@ def save_environment_json(self): 'version': self.config.python_version, }, 'build': { - 'image': self.config.build_image, + # TODO: remove getattr when https://github.com/rtfd/readthedocs.org/pull/3339 got merged + 'image': getattr(self.config, 'build_image', self.version.project.container_image), }, } with open(self.environment_json_path(), 'w') as fpath: From e99673a4932d7fc2d3f8c4689bd2c43cdcf8bbec Mon Sep 17 00:00:00 2001 From: Manuel Kaufmann Date: Thu, 21 Dec 2017 17:29:45 -0500 Subject: [PATCH 05/12] Minor refactor --- .../doc_builder/python_environments.py | 23 +++++++++++-------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/readthedocs/doc_builder/python_environments.py b/readthedocs/doc_builder/python_environments.py index 6fc5ce1101e..adb5ee44eae 100644 --- a/readthedocs/doc_builder/python_environments.py +++ b/readthedocs/doc_builder/python_environments.py @@ -11,9 +11,9 @@ from builtins import object from django.conf import settings -from packaging.version import Version from readthedocs.doc_builder.config import ConfigWrapper +from readthedocs.doc_builder.constants import DOCKER_IMAGE from readthedocs.doc_builder.loader import get_builder_class from readthedocs.projects.constants import LOG_TEMPLATE from readthedocs.projects.models import Feature @@ -51,7 +51,7 @@ def delete_existing_build_dir(self): self._log('Removing existing build directory') shutil.rmtree(build_dir) - def delete_existing_env_dir(self): + def delete_existing_venv_dir(self): venv_dir = self.venv_path() # Handle deleting old venv dir if os.path.exists(venv_dir): @@ -130,32 +130,37 @@ def is_obsolete(self): try: with open(self.environment_json_path(), 'r') as fpath: environment_conf = json.load(fpath) - env_python_version = Version(environment_conf['python']['version']) - env_build_image = Version(environment_conf['build']['image']) + env_python_version = environment_conf['python']['version'] + env_build_image = environment_conf['build']['image'] except (TypeError, KeyError, ValueError): + log.error('Unable to read/parse environment.json file') return False + # TODO: remove getattr when https://github.com/rtfd/readthedocs.org/pull/3339 got merged + build_image = getattr(self.config, 'build_image', self.version.project.container_image) or DOCKER_IMAGE + # If the user define the Python version just as a major version # (e.g. ``2`` or ``3``) we won't know exactly which exact version was # used to create the venv but we can still compare it against the new # one coming from the project version config. return any([ - env_python_version != Version(self.config.python_version), - # TODO: remove getattr when https://github.com/rtfd/readthedocs.org/pull/3339 got merged - env_build_image != getattr(self.config, 'build_image', self.version.project.container_image), + env_python_version != self.config.python_version, + env_build_image != build_image, ]) def save_environment_json(self): """ Save on disk Python and build image versions used to create the venv. """ + # TODO: remove getattr when https://github.com/rtfd/readthedocs.org/pull/3339 got merged + build_image = getattr(self.config, 'build_image', self.version.project.container_image) or DOCKER_IMAGE + data = { 'python': { 'version': self.config.python_version, }, 'build': { - # TODO: remove getattr when https://github.com/rtfd/readthedocs.org/pull/3339 got merged - 'image': getattr(self.config, 'build_image', self.version.project.container_image), + 'image': build_image, }, } with open(self.environment_json_path(), 'w') as fpath: From d38b5c340aba627ec577f970178679d1c40922f6 Mon Sep 17 00:00:00 2001 From: Manuel Kaufmann Date: Thu, 21 Dec 2017 17:44:33 -0500 Subject: [PATCH 06/12] Fix linting errors --- readthedocs/doc_builder/python_environments.py | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/readthedocs/doc_builder/python_environments.py b/readthedocs/doc_builder/python_environments.py index adb5ee44eae..8cab79aa3d7 100644 --- a/readthedocs/doc_builder/python_environments.py +++ b/readthedocs/doc_builder/python_environments.py @@ -99,9 +99,7 @@ def venv_bin(self, filename=None): return os.path.join(*parts) def environment_json_path(self): - """ - Return the path to the ``environment.json`` file for this venv. - """ + """Return the path to the ``environment.json`` file for this venv.""" return os.path.join( self.venv_path(), 'environment.json', @@ -137,7 +135,7 @@ def is_obsolete(self): return False # TODO: remove getattr when https://github.com/rtfd/readthedocs.org/pull/3339 got merged - build_image = getattr(self.config, 'build_image', self.version.project.container_image) or DOCKER_IMAGE + build_image = getattr(self.config, 'build_image', self.version.project.container_image) or DOCKER_IMAGE # noqa # If the user define the Python version just as a major version # (e.g. ``2`` or ``3``) we won't know exactly which exact version was @@ -149,11 +147,9 @@ def is_obsolete(self): ]) def save_environment_json(self): - """ - Save on disk Python and build image versions used to create the venv. - """ + """Save on disk Python and build image versions used to create the venv.""" # TODO: remove getattr when https://github.com/rtfd/readthedocs.org/pull/3339 got merged - build_image = getattr(self.config, 'build_image', self.version.project.container_image) or DOCKER_IMAGE + build_image = getattr(self.config, 'build_image', self.version.project.container_image) or DOCKER_IMAGE # noqa data = { 'python': { @@ -167,7 +163,6 @@ def save_environment_json(self): json.dump(data, fpath) - class Virtualenv(PythonEnvironment): """ From b7a4cc1089398ee6344decdb9f8ebf2135a8453d Mon Sep 17 00:00:00 2001 From: Manuel Kaufmann Date: Thu, 21 Dec 2017 19:55:15 -0500 Subject: [PATCH 07/12] Test cases for PythonEnvironment.is_obsolete --- .../doc_builder/python_environments.py | 4 +- .../rtd_tests/tests/test_doc_building.py | 111 +++++++++++++++++- 2 files changed, 112 insertions(+), 3 deletions(-) diff --git a/readthedocs/doc_builder/python_environments.py b/readthedocs/doc_builder/python_environments.py index 8cab79aa3d7..343e29b5de2 100644 --- a/readthedocs/doc_builder/python_environments.py +++ b/readthedocs/doc_builder/python_environments.py @@ -8,7 +8,7 @@ import logging import os import shutil -from builtins import object +from builtins import object, open from django.conf import settings @@ -130,7 +130,7 @@ def is_obsolete(self): environment_conf = json.load(fpath) env_python_version = environment_conf['python']['version'] env_build_image = environment_conf['build']['image'] - except (TypeError, KeyError, ValueError): + except (IOError, TypeError, KeyError, ValueError): log.error('Unable to read/parse environment.json file') return False diff --git a/readthedocs/rtd_tests/tests/test_doc_building.py b/readthedocs/rtd_tests/tests/test_doc_building.py index 818acaeb75a..cec2fe0c170 100644 --- a/readthedocs/rtd_tests/tests/test_doc_building.py +++ b/readthedocs/rtd_tests/tests/test_doc_building.py @@ -14,18 +14,22 @@ from builtins import str import mock +import pytest from django.test import TestCase from docker.errors import APIError as DockerAPIError from docker.errors import DockerException -from mock import Mock, PropertyMock, patch +from mock import Mock, PropertyMock, mock_open, patch from readthedocs.builds.constants import BUILD_STATE_CLONING from readthedocs.builds.models import Version +from readthedocs.doc_builder.config import ConfigWrapper from readthedocs.doc_builder.environments import ( BuildCommand, DockerBuildCommand, DockerEnvironment, LocalEnvironment) from readthedocs.doc_builder.exceptions import BuildEnvironmentError +from readthedocs.doc_builder.python_environments import Virtualenv from readthedocs.projects.models import Project from readthedocs.rtd_tests.mocks.environment import EnvironmentMockGroup +from readthedocs.rtd_tests.tests.test_config_wrapper import create_load DUMMY_BUILD_ID = 123 SAMPLE_UNICODE = u'HérÉ îß sömê ünïçó∂é' @@ -831,3 +835,108 @@ def test_command_oom_kill(self): self.assertEqual( str(cmd.output), u'Command killed due to excessive memory consumption\n') + + + + +class TestAutoWipeEnvironment(TestCase): + fixtures = ['test_data'] + + def setUp(self): + self.pip = Project.objects.get(slug='pip') + self.version = self.pip.versions.get(slug='0.8') + + def test_is_obsolete_without_env_json_file(self): + yaml_config = create_load()()[0] + config = ConfigWrapper(version=self.version, yaml_config=yaml_config) + + with patch('os.path.exists') as exists: + exists.return_value = False + python_env = Virtualenv( + version=self.version, + build_env=None, + config=config, + ) + + self.assertFalse(python_env.is_obsolete) + + def test_is_obsolete_with_invalid_env_json_file(self): + yaml_config = create_load()()[0] + config = ConfigWrapper(version=self.version, yaml_config=yaml_config) + + with patch('os.path.exists') as exists: + exists.return_value = True + python_env = Virtualenv( + version=self.version, + build_env=None, + config=config, + ) + + self.assertFalse(python_env.is_obsolete) + + def test_is_obsolete_with_json_different_python_version(self): + config_data = { + 'build': { + 'image': '2.0', + }, + 'python': { + 'version': 2.7, + }, + } + yaml_config = create_load(config_data)()[0] + config = ConfigWrapper(version=self.version, yaml_config=yaml_config) + + python_env = Virtualenv( + version=self.version, + build_env=None, + config=config, + ) + env_json_data = '{"build": {"image": "readthedocs/build:2.0"}, "python": {"version": 3.5}}' + 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.assertTrue(python_env.is_obsolete) + + @pytest.mark.xfail(reason='build.image is not being considered yet') + def test_is_obsolete_with_json_different_build_image(self): + config_data = { + 'build': { + 'image': 'latest', + }, + 'python': { + 'version': 2.7, + }, + } + yaml_config = create_load(config_data)()[0] + config = ConfigWrapper(version=self.version, yaml_config=yaml_config) + + python_env = Virtualenv( + version=self.version, + build_env=None, + config=config, + ) + env_json_data = '{"build": {"image": "readthedocs/build:2.0"}, "python": {"version": 2.7}}' + 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.assertTrue(python_env.is_obsolete) + + def test_is_obsolete_with_json_same_data_as_version(self): + config_data = { + 'build': { + 'image': '2.0', + }, + 'python': { + 'version': 3.5, + }, + } + yaml_config = create_load(config_data)()[0] + config = ConfigWrapper(version=self.version, yaml_config=yaml_config) + + python_env = Virtualenv( + version=self.version, + build_env=None, + config=config, + ) + env_json_data = '{"build": {"image": "readthedocs/build:2.0"}, "python": {"version": 3.5}}' + 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) From 49ed94332e00ae7d93a47843400f8c53f4c1c932 Mon Sep 17 00:00:00 2001 From: Manuel Kaufmann Date: Fri, 22 Dec 2017 18:56:48 -0500 Subject: [PATCH 08/12] Comment fix --- readthedocs/doc_builder/python_environments.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/readthedocs/doc_builder/python_environments.py b/readthedocs/doc_builder/python_environments.py index 343e29b5de2..473c18f0570 100644 --- a/readthedocs/doc_builder/python_environments.py +++ b/readthedocs/doc_builder/python_environments.py @@ -119,7 +119,7 @@ def is_obsolete(self): :rtype: bool """ - # Always return True if we don't have information about what Python + # Always returns False if we don't have information about what Python # version/Docker image was used to create the venv as backward # compatibility. if not os.path.exists(self.environment_json_path()): From da1fde31d1756cb3f2790f0f5c753c7c1c0cc595 Mon Sep 17 00:00:00 2001 From: Manuel Kaufmann Date: Fri, 22 Dec 2017 18:59:50 -0500 Subject: [PATCH 09/12] Rename environment file --- readthedocs/doc_builder/python_environments.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/readthedocs/doc_builder/python_environments.py b/readthedocs/doc_builder/python_environments.py index 473c18f0570..d7886e514df 100644 --- a/readthedocs/doc_builder/python_environments.py +++ b/readthedocs/doc_builder/python_environments.py @@ -99,10 +99,10 @@ def venv_bin(self, filename=None): return os.path.join(*parts) def environment_json_path(self): - """Return the path to the ``environment.json`` file for this venv.""" + """Return the path to the ``readthedocs-environment.json`` file.""" return os.path.join( self.venv_path(), - 'environment.json', + 'readthedocs-environment.json', ) @property @@ -110,10 +110,10 @@ def is_obsolete(self): """ Determine if the Python version of the venv obsolete. - It checks the the data stored at ``environment.json`` and compares it - against the Python version in the project version to be built and the - Docker image used to create the venv against the one in the project - version config. + It checks the the data stored at ``readthedocs-environment.json`` and + compares it against the Python version in the project version to be + built and the Docker image used to create the venv against the one in + the project version config. :returns: ``True`` when it's obsolete and ``False`` otherwise @@ -131,7 +131,7 @@ def is_obsolete(self): env_python_version = environment_conf['python']['version'] env_build_image = environment_conf['build']['image'] except (IOError, TypeError, KeyError, ValueError): - log.error('Unable to read/parse environment.json file') + log.error('Unable to read/parse readthedocs-environment.json file') return False # TODO: remove getattr when https://github.com/rtfd/readthedocs.org/pull/3339 got merged From 3b2f538cc218905bdc591d4b7d6ec6a9ad76dc82 Mon Sep 17 00:00:00 2001 From: Manuel Kaufmann Date: Wed, 27 Dec 2017 20:53:45 -0500 Subject: [PATCH 10/12] Make json.dump compatible with py2 and py3 --- readthedocs/doc_builder/python_environments.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/readthedocs/doc_builder/python_environments.py b/readthedocs/doc_builder/python_environments.py index d7886e514df..0e087e495ea 100644 --- a/readthedocs/doc_builder/python_environments.py +++ b/readthedocs/doc_builder/python_environments.py @@ -159,8 +159,11 @@ def save_environment_json(self): 'image': build_image, }, } + with open(self.environment_json_path(), 'w') as fpath: - json.dump(data, fpath) + # Compatibility for Py2 and Py3. ``io.TextIOWrapper`` expects + # unicode but ``json.dumps`` returns str in Py2. + fpath.write(unicode(json.dumps(data))) class Virtualenv(PythonEnvironment): From a151cdeafaadc3f07eb36ee0ac7bbfedbf1439b8 Mon Sep 17 00:00:00 2001 From: Manuel Kaufmann Date: Wed, 27 Dec 2017 20:58:04 -0500 Subject: [PATCH 11/12] Use `config.python_full_version` instead of `config.python_version` --- readthedocs/doc_builder/config.py | 25 +++++++++++-------- .../doc_builder/python_environments.py | 4 +-- 2 files changed, 17 insertions(+), 12 deletions(-) diff --git a/readthedocs/doc_builder/config.py b/readthedocs/doc_builder/config.py index 2ce1db0ad9c..ae0fd84b99e 100644 --- a/readthedocs/doc_builder/config.py +++ b/readthedocs/doc_builder/config.py @@ -51,16 +51,7 @@ def extra_requirements(self): @property def python_interpreter(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( - list( - filter( - lambda x: x < ver + 1, - self._yaml_config.get_valid_python_versions(), - ))) + ver = self.python_full_version return 'python{0}'.format(ver) @property @@ -75,6 +66,20 @@ def python_version(self): version = 3 return version + @property + 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( + list( + filter( + lambda x: x < ver + 1, + self._yaml_config.get_valid_python_versions(), + ))) + return ver + @property def use_system_site_packages(self): if 'use_system_site_packages' in self._yaml_config.get('python', {}): diff --git a/readthedocs/doc_builder/python_environments.py b/readthedocs/doc_builder/python_environments.py index 0e087e495ea..b64c8bbce35 100644 --- a/readthedocs/doc_builder/python_environments.py +++ b/readthedocs/doc_builder/python_environments.py @@ -142,7 +142,7 @@ def is_obsolete(self): # used to create the venv but we can still compare it against the new # one coming from the project version config. return any([ - env_python_version != self.config.python_version, + env_python_version != self.config.python_full_version, env_build_image != build_image, ]) @@ -153,7 +153,7 @@ def save_environment_json(self): data = { 'python': { - 'version': self.config.python_version, + 'version': self.config.python_full_version, }, 'build': { 'image': build_image, From f31144a87df2e21c22932f5f3eee337fa9f69157 Mon Sep 17 00:00:00 2001 From: Manuel Kaufmann Date: Wed, 27 Dec 2017 21:12:43 -0500 Subject: [PATCH 12/12] Test for changing container_image at project level --- .../rtd_tests/tests/test_doc_building.py | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/readthedocs/rtd_tests/tests/test_doc_building.py b/readthedocs/rtd_tests/tests/test_doc_building.py index cec2fe0c170..8e928d27dae 100644 --- a/readthedocs/rtd_tests/tests/test_doc_building.py +++ b/readthedocs/rtd_tests/tests/test_doc_building.py @@ -919,6 +919,32 @@ def test_is_obsolete_with_json_different_build_image(self): exists.return_value = True self.assertTrue(python_env.is_obsolete) + def test_is_obsolete_with_project_different_build_image(self): + config_data = { + 'build': { + 'image': '2.0', + }, + 'python': { + 'version': 2.7, + }, + } + yaml_config = create_load(config_data)()[0] + config = ConfigWrapper(version=self.version, yaml_config=yaml_config) + + # Set container_image manually + self.pip.container_image = 'readthedocs/build:latest' + self.pip.save() + + python_env = Virtualenv( + version=self.version, + build_env=None, + config=config, + ) + env_json_data = '{"build": {"image": "readthedocs/build:2.0"}, "python": {"version": 2.7}}' + 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.assertTrue(python_env.is_obsolete) + def test_is_obsolete_with_json_same_data_as_version(self): config_data = { 'build': {