Repository: conda/conda

Issue:
Prevent conda from scheduling downgrades of conda when no version is explicitly given
To go along with https://github.com/conda/conda/pull/7517.  See https://groups.google.com/a/anaconda.com/forum/?utm_medium=email&utm_source=footer#!msg/conda-builders/ydua189zWcg/rz3I99JzAgAJ


Discussion:

Resolving patch:
diff --git a/conda/core/solve.py b/conda/core/solve.py
--- a/conda/core/solve.py
+++ b/conda/core/solve.py
@@ -288,9 +288,6 @@ def solve_final_state(self, deps_modifier=NULL, prune=NULL, ignore_pinned=NULL,
                 if spec.name in specs_map:
                     old_spec = specs_map[spec.name]
                     specs_map[spec.name] = MatchSpec(old_spec, target=None)
-            if (context.auto_update_conda and paths_equal(self.prefix, context.root_prefix)
-                    and any(dist.name == "conda" for dist in solution)):
-                specs_map["conda"] = MatchSpec("conda")
 
         # add in explicitly requested specs from specs_to_add
         # this overrides any name-matching spec already in the spec map
@@ -303,6 +300,21 @@ def solve_final_state(self, deps_modifier=NULL, prune=NULL, ignore_pinned=NULL,
         if not ignore_pinned:
             pinned_specs = get_pinned_specs(self.prefix)
 
+        # As a business rule, we never want to downgrade conda below the current version,
+        # unless that's requested explicitly by the user (which we actively discourage).
+        if 'conda' in specs_map and paths_equal(self.prefix, context.conda_prefix):
+            conda_prefix_rec = prefix_data.get('conda')
+            if conda_prefix_rec:
+                conda_spec = specs_map['conda']
+                conda_in_specs_to_add_version = next(
+                    (spec.get('version') for spec in specs_to_add if spec.name == "conda"), None
+                )
+                if not conda_in_specs_to_add_version:
+                    conda_spec = MatchSpec(conda_spec, version=">=%s" % conda_prefix_rec.version)
+                if context.auto_update_conda:
+                    conda_spec = MatchSpec(conda_spec, target=None)
+                specs_map['conda'] = conda_spec
+
         final_environment_specs = IndexedSet(concatv(
             itervalues(specs_map),
             track_features_specs,


Test patch:
diff --git a/tests/core/test_solve.py b/tests/core/test_solve.py
--- a/tests/core/test_solve.py
+++ b/tests/core/test_solve.py
@@ -3,6 +3,8 @@
 
 from contextlib import contextmanager
 import os
+from pprint import pprint
+import sys
 from unittest import TestCase
 
 from os.path import join
@@ -681,6 +683,139 @@ def test_broken_install():
     #     assert r.environment_is_consistent(order)
 
 
+def test_conda_downgrade():
+    specs = MatchSpec("conda-build"),
+    with env_var("CONDA_CHANNEL_PRIORITY", "False", reset_context):
+        with get_solver_aggregate_1(specs) as solver:
+            final_state_1 = solver.solve_final_state()
+            # PrefixDag(final_state_1, specs).open_url()
+            pprint([Dist(rec).full_name for rec in final_state_1])
+            order = (
+                'channel-4::ca-certificates-2017.08.26-h1d4fec5_0',
+                'channel-2::conda-env-2.6.0-0',
+                'channel-2::libffi-3.2.1-1',
+                'channel-4::libgcc-ng-7.2.0-h7cc24e2_2',
+                'channel-4::libstdcxx-ng-7.2.0-h7a57d05_2',
+                'channel-4::ncurses-6.0-h9df7e31_2',
+                'channel-4::openssl-1.0.2n-hb7f436b_0',
+                'channel-4::patchelf-0.9-hf79760b_2',
+                'channel-4::tk-8.6.7-hc745277_3',
+                'channel-4::xz-5.2.3-h55aa19d_2',
+                'channel-4::yaml-0.1.7-had09818_2',
+                'channel-4::zlib-1.2.11-ha838bed_2',
+                'channel-4::libedit-3.1-heed3624_0',
+                'channel-4::readline-7.0-ha6073c6_4',
+                'channel-4::sqlite-3.22.0-h1bed415_0',
+                'channel-4::python-3.6.4-hc3d631a_1',
+                'channel-4::asn1crypto-0.24.0-py36_0',
+                'channel-4::beautifulsoup4-4.6.0-py36h49b8c8c_1',
+                'channel-4::certifi-2018.1.18-py36_0',
+                'channel-4::chardet-3.0.4-py36h0f667ec_1',
+                'channel-4::filelock-3.0.4-py36_0',
+                'channel-4::glob2-0.6-py36he249c77_0',
+                'channel-4::idna-2.6-py36h82fb2a8_1',
+                'channel-4::markupsafe-1.0-py36hd9260cd_1',
+                'channel-4::pkginfo-1.4.1-py36h215d178_1',
+                'channel-4::psutil-5.4.3-py36h14c3975_0',
+                'channel-4::pycosat-0.6.3-py36h0a5515d_0',
+                'channel-4::pycparser-2.18-py36hf9f622e_1',
+                'channel-4::pysocks-1.6.7-py36hd97a5b1_1',
+                'channel-4::pyyaml-3.12-py36hafb9ca4_1',
+                'channel-4::ruamel_yaml-0.15.35-py36h14c3975_1',
+                'channel-4::six-1.11.0-py36h372c433_1',
+                'channel-4::cffi-1.11.4-py36h9745a5d_0',
+                'channel-4::conda-verify-2.0.0-py36h98955d8_0',
+                'channel-4::setuptools-38.5.1-py36_0',
+                'channel-4::cryptography-2.1.4-py36hd09be54_0',
+                'channel-4::jinja2-2.10-py36ha16c418_0',
+                'channel-4::pyopenssl-17.5.0-py36h20ba746_0',
+                'channel-4::urllib3-1.22-py36hbe7ace6_0',
+                'channel-4::requests-2.18.4-py36he2e5f8d_1',
+                'channel-4::conda-4.4.10-py36_0',
+                'channel-4::conda-build-3.5.1-py36_0',
+            )
+            assert tuple(final_state_1) == tuple(solver._index[Dist(d)] for d in order)
+
+    specs_to_add = MatchSpec("itsdangerous"),  # MatchSpec("conda"),
+    saved_sys_prefix = sys.prefix
+    try:
+        sys.prefix = TEST_PREFIX
+        with get_solver_aggregate_1(specs_to_add=specs_to_add, prefix_records=final_state_1,
+                                    history_specs=specs) as solver:
+            unlink_precs, link_precs = solver.solve_for_diff()
+            # PrefixDag(final_state_1, specs).open_url()
+            pprint([Dist(rec).full_name for rec in unlink_precs])
+            pprint([Dist(rec).full_name for rec in link_precs])
+            unlink_order = (
+                'channel-4::requests-2.18.4-py36he2e5f8d_1',
+                'channel-4::pyopenssl-17.5.0-py36h20ba746_0',
+                'channel-4::cryptography-2.1.4-py36hd09be54_0',
+                'channel-4::setuptools-38.5.1-py36_0',
+                'channel-4::certifi-2018.1.18-py36_0',
+                'channel-4::python-3.6.4-hc3d631a_1',
+                'channel-4::openssl-1.0.2n-hb7f436b_0',
+                # no conda downgrade
+            )
+            link_order = (
+                'channel-2::openssl-1.0.2l-0',
+                'channel-4::python-3.6.3-h0ef2715_3',
+                'channel-2::certifi-2016.2.28-py36_0',
+                'channel-2::itsdangerous-0.24-py36_0',
+                'channel-2::pyparsing-2.1.4-py36_0',
+                'channel-2::requests-2.14.2-py36_0',
+                'channel-2::packaging-16.8-py36_0',
+                'channel-4::setuptools-36.5.0-py36he42e2e1_0',
+                'channel-2::cryptography-1.8.1-py36_0',
+                'channel-2::pyopenssl-17.0.0-py36_0',
+            )
+            assert tuple(unlink_precs) == tuple(solver._index[Dist(d)] for d in unlink_order)
+            assert tuple(link_precs) == tuple(solver._index[Dist(d)] for d in link_order)
+
+        specs_to_add = MatchSpec("itsdangerous"), MatchSpec("conda"),
+        with get_solver_aggregate_1(specs_to_add=specs_to_add, prefix_records=final_state_1,
+                                    history_specs=specs) as solver:
+            unlink_precs, link_precs = solver.solve_for_diff()
+            pprint([Dist(rec).full_name for rec in unlink_precs])
+            pprint([Dist(rec).full_name for rec in link_precs])
+            assert tuple(unlink_precs) == tuple(solver._index[Dist(d)] for d in unlink_order)
+            assert tuple(link_precs) == tuple(solver._index[Dist(d)] for d in link_order)
+
+        specs_to_add = MatchSpec("itsdangerous"), MatchSpec("conda<4.4.10"),
+        with get_solver_aggregate_1(specs_to_add=specs_to_add, prefix_records=final_state_1,
+                                    history_specs=specs) as solver:
+            unlink_precs, link_precs = solver.solve_for_diff()
+            pprint([Dist(rec).full_name for rec in unlink_precs])
+            pprint([Dist(rec).full_name for rec in link_precs])
+            unlink_order = (
+                # now conda gets downgraded
+                'channel-4::conda-4.4.10-py36_0',
+                'channel-4::requests-2.18.4-py36he2e5f8d_1',
+                'channel-4::pyopenssl-17.5.0-py36h20ba746_0',
+                'channel-4::cryptography-2.1.4-py36hd09be54_0',
+                'channel-4::setuptools-38.5.1-py36_0',
+                'channel-4::certifi-2018.1.18-py36_0',
+                'channel-4::python-3.6.4-hc3d631a_1',
+                'channel-4::openssl-1.0.2n-hb7f436b_0',
+            )
+            link_order = (
+                'channel-2::openssl-1.0.2l-0',
+                'channel-4::python-3.6.3-h0ef2715_3',
+                'channel-2::certifi-2016.2.28-py36_0',
+                'channel-2::itsdangerous-0.24-py36_0',
+                'channel-2::pyparsing-2.1.4-py36_0',
+                'channel-2::requests-2.14.2-py36_0',
+                'channel-2::setuptools-27.2.0-py36_0',
+                'channel-2::packaging-16.8-py36_0',
+                'channel-2::cryptography-1.8.1-py36_0',
+                'channel-2::pyopenssl-17.0.0-py36_0',
+                'channel-2::conda-4.3.21-py36_0',
+            )
+            assert tuple(unlink_precs) == tuple(solver._index[Dist(d)] for d in unlink_order)
+            assert tuple(link_precs) == tuple(solver._index[Dist(d)] for d in link_order)
+    finally:
+        sys.prefix = saved_sys_prefix
+
+
 def test_install_uninstall_features_1():
     specs = MatchSpec("pandas"), MatchSpec("python=2.7"), MatchSpec("numpy 1.6.*")
     with env_var("CONDA_TRACK_FEATURES", 'mkl', reset_context):
@@ -891,47 +1026,52 @@ def test_auto_update_conda():
             )
             assert tuple(final_state_2) == tuple(solver._index[Dist(d)] for d in order)
 
-    with env_vars({"CONDA_AUTO_UPDATE_CONDA": "yes", "CONDA_ROOT_PREFIX": TEST_PREFIX}, reset_context):
-        specs_to_add = MatchSpec("pytz"),
-        with get_solver(specs_to_add, prefix_records=final_state_1, history_specs=specs) as solver:
-            final_state_2 = solver.solve_final_state()
-            # PrefixDag(final_state_2, specs).open_url()
-            print([Dist(rec).full_name for rec in final_state_2])
-            order = (
-                'channel-1::openssl-1.0.1c-0',
-                'channel-1::readline-6.2-0',
-                'channel-1::sqlite-3.7.13-0',
-                'channel-1::system-5.8-1',
-                'channel-1::tk-8.5.13-0',
-                'channel-1::yaml-0.1.4-0',
-                'channel-1::zlib-1.2.7-0',
-                'channel-1::python-2.7.5-0',
-                'channel-1::pytz-2013b-py27_0',
-                'channel-1::pyyaml-3.10-py27_0',
-                'channel-1::conda-1.5.2-py27_0',
-            )
-            assert tuple(final_state_2) == tuple(solver._index[Dist(d)] for d in order)
-
-    with env_vars({"CONDA_AUTO_UPDATE_CONDA": "no", "CONDA_ROOT_PREFIX": TEST_PREFIX}, reset_context):
-        specs_to_add = MatchSpec("pytz"),
-        with get_solver(specs_to_add, prefix_records=final_state_1, history_specs=specs) as solver:
-            final_state_2 = solver.solve_final_state()
-            # PrefixDag(final_state_2, specs).open_url()
-            print([Dist(rec).full_name for rec in final_state_2])
-            order = (
-                'channel-1::openssl-1.0.1c-0',
-                'channel-1::readline-6.2-0',
-                'channel-1::sqlite-3.7.13-0',
-                'channel-1::system-5.8-1',
-                'channel-1::tk-8.5.13-0',
-                'channel-1::yaml-0.1.4-0',
-                'channel-1::zlib-1.2.7-0',
-                'channel-1::python-2.7.5-0',
-                'channel-1::pytz-2013b-py27_0',
-                'channel-1::pyyaml-3.10-py27_0',
-                'channel-1::conda-1.3.5-py27_0',
-            )
-            assert tuple(final_state_2) == tuple(solver._index[Dist(d)] for d in order)
+    saved_sys_prefix = sys.prefix
+    try:
+        sys.prefix = TEST_PREFIX
+        with env_vars({"CONDA_AUTO_UPDATE_CONDA": "yes"}, reset_context):
+            specs_to_add = MatchSpec("pytz"),
+            with get_solver(specs_to_add, prefix_records=final_state_1, history_specs=specs) as solver:
+                final_state_2 = solver.solve_final_state()
+                # PrefixDag(final_state_2, specs).open_url()
+                print([Dist(rec).full_name for rec in final_state_2])
+                order = (
+                    'channel-1::openssl-1.0.1c-0',
+                    'channel-1::readline-6.2-0',
+                    'channel-1::sqlite-3.7.13-0',
+                    'channel-1::system-5.8-1',
+                    'channel-1::tk-8.5.13-0',
+                    'channel-1::yaml-0.1.4-0',
+                    'channel-1::zlib-1.2.7-0',
+                    'channel-1::python-2.7.5-0',
+                    'channel-1::pytz-2013b-py27_0',
+                    'channel-1::pyyaml-3.10-py27_0',
+                    'channel-1::conda-1.5.2-py27_0',
+                )
+                assert tuple(final_state_2) == tuple(solver._index[Dist(d)] for d in order)
+
+        with env_vars({"CONDA_AUTO_UPDATE_CONDA": "no"}, reset_context):
+            specs_to_add = MatchSpec("pytz"),
+            with get_solver(specs_to_add, prefix_records=final_state_1, history_specs=specs) as solver:
+                final_state_2 = solver.solve_final_state()
+                # PrefixDag(final_state_2, specs).open_url()
+                print([Dist(rec).full_name for rec in final_state_2])
+                order = (
+                    'channel-1::openssl-1.0.1c-0',
+                    'channel-1::readline-6.2-0',
+                    'channel-1::sqlite-3.7.13-0',
+                    'channel-1::system-5.8-1',
+                    'channel-1::tk-8.5.13-0',
+                    'channel-1::yaml-0.1.4-0',
+                    'channel-1::zlib-1.2.7-0',
+                    'channel-1::python-2.7.5-0',
+                    'channel-1::pytz-2013b-py27_0',
+                    'channel-1::pyyaml-3.10-py27_0',
+                    'channel-1::conda-1.3.5-py27_0',
+                )
+                assert tuple(final_state_2) == tuple(solver._index[Dist(d)] for d in order)
+    finally:
+        sys.prefix = saved_sys_prefix
 
 
 def test_aggressive_update_packages():