Repository: conda/conda

Issue:
make sure conda 4.4 has soft-pinning for python
https://github.com/conda/conda/issues/6164#issuecomment-336733311


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
@@ -1,12 +1,11 @@
 # -*- coding: utf-8 -*-
 from __future__ import absolute_import, division, print_function, unicode_literals
 
+from enum import Enum
 from genericpath import exists
 from logging import DEBUG, getLogger
 from os.path import join
 
-from enum import Enum
-
 from .index import get_reduced_index
 from .link import PrefixSetup, UnlinkLinkTransaction
 from .linked_data import PrefixData, linked_data
@@ -15,7 +14,7 @@
 from ..common.compat import iteritems, itervalues, odict, string_types, text_type
 from ..common.constants import NULL
 from ..common.io import spinner
-from ..common.path import paths_equal
+from ..common.path import get_major_minor_version, paths_equal
 from ..exceptions import PackagesNotFoundError
 from ..gateways.logging import TRACE
 from ..history import History
@@ -235,6 +234,16 @@ def solve_final_state(self, deps_modifier=NULL, prune=NULL, ignore_pinned=NULL,
             specs_map = {pkg_name: MatchSpec(spec.name, optional=spec.optional)
                          for pkg_name, spec in iteritems(specs_map)}
 
+        # As a business rule, we never want to update python beyond the current minor version,
+        # unless that's requested explicitly by the user (which we actively discourage).
+        if 'python' in specs_map:
+            python_prefix_rec = prefix_data.get('python')
+            if python_prefix_rec:
+                python_spec = specs_map['python']
+                if not python_spec.get('version'):
+                    pinned_version = get_major_minor_version(python_prefix_rec.version) + '.*'
+                    specs_map['python'] = MatchSpec(python_spec, version=pinned_version)
+
         # For the aggressive_update_packages configuration parameter, we strip any target
         # that's been set.
         if not context.offline:


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
@@ -467,12 +467,11 @@ def test_update_all_1():
             'channel-1::tk-8.5.13-0',
             'channel-1::zlib-1.2.7-0',
             'channel-1::llvm-3.2-0',
-            'channel-1::python-2.7.5-0',
-            'channel-1::llvmpy-0.10.2-py27_0',
-            'channel-1::meta-0.4.2.dev-py27_0',
-            'channel-1::nose-1.3.0-py27_0',
-            'channel-1::numpy-1.7.1-py27_0',
-            'channel-1::numba-0.6.0-np17py27_0'
+            'channel-1::python-2.6.8-6',  # stick with python=2.6 even though UPDATE_ALL
+            'channel-1::llvmpy-0.10.2-py26_0',
+            'channel-1::nose-1.3.0-py26_0',
+            'channel-1::numpy-1.7.1-py26_0',
+            'channel-1::numba-0.6.0-np17py26_0',
         )
         assert tuple(final_state_2) == tuple(solver._index[Dist(d)] for d in order)
 
@@ -1336,7 +1335,6 @@ def test_freeze_deps_1():
         )
         assert tuple(final_state_1) == tuple(solver._index[Dist(d)] for d in order)
 
-    # to keep six=1.7 as a requested spec, we have to downgrade python to 2.7
     specs_to_add = MatchSpec("bokeh"),
     with get_solver_2(specs_to_add, prefix_records=final_state_1, history_specs=specs) as solver:
         final_state_2 = solver.solve_final_state()
@@ -1351,23 +1349,18 @@ def test_freeze_deps_1():
             'channel-2::xz-5.2.2-1',
             'channel-2::yaml-0.1.6-0',
             'channel-2::zlib-1.2.8-3',
-            'channel-2::python-2.7.13-0',
-            'channel-2::backports-1.0-py27_0',
-            'channel-2::backports_abc-0.5-py27_0',
-            'channel-2::futures-3.1.1-py27_0',
-            'channel-2::markupsafe-0.23-py27_2',
-            'channel-2::numpy-1.13.0-py27_0',
-            'channel-2::pyyaml-3.12-py27_0',
-            'channel-2::requests-2.14.2-py27_0',
-            'channel-2::setuptools-27.2.0-py27_0',
-            'channel-2::six-1.7.3-py27_0',
-            'channel-2::bkcharts-0.2-py27_0',
-            'channel-2::jinja2-2.9.6-py27_0',
-            'channel-2::python-dateutil-2.6.0-py27_0',
-            'channel-2::singledispatch-3.4.0.3-py27_0',
-            'channel-2::ssl_match_hostname-3.4.0.2-py27_1',
-            'channel-2::tornado-4.5.1-py27_0',
-            'channel-2::bokeh-0.12.6-py27_0',
+            'channel-2::python-3.4.5-0',
+            'channel-2::backports_abc-0.5-py34_0',
+            'channel-2::markupsafe-0.23-py34_2',
+            'channel-2::numpy-1.13.0-py34_0',
+            'channel-2::pyyaml-3.12-py34_0',
+            'channel-2::requests-2.14.2-py34_0',
+            'channel-2::setuptools-27.2.0-py34_0',
+            'channel-2::six-1.7.3-py34_0',
+            'channel-2::jinja2-2.9.6-py34_0',
+            'channel-2::python-dateutil-2.6.0-py34_0',
+            'channel-2::tornado-4.4.2-py34_0',
+            'channel-2::bokeh-0.12.4-py34_0',
         )
         assert tuple(final_state_2) == tuple(solver._index[Dist(d)] for d in order)
 
diff --git a/tests/test_create.py b/tests/test_create.py
--- a/tests/test_create.py
+++ b/tests/test_create.py
@@ -898,13 +898,13 @@ def test_package_pinning(self):
             run_command(Commands.UPDATE, prefix, "--all")
             assert package_is_installed(prefix, "itsdangerous-0.23")
             # assert not package_is_installed(prefix, "python-3.5")  # should be python-3.6, but it's not because of add_defaults_to_specs
-            assert not package_is_installed(prefix, "python-2.7")  # add_defaults_to_specs is right now removed for python pinning, TODO: discuss
+            assert package_is_installed(prefix, "python-2.7")
 
             assert not package_is_installed(prefix, "pytz-2015.7")
             assert package_is_installed(prefix, "pytz-")
 
             run_command(Commands.UPDATE, prefix, "--all --no-pin")
-            assert not package_is_installed(prefix, "python-2.7")
+            assert package_is_installed(prefix, "python-2.7")
             assert not package_is_installed(prefix, "itsdangerous-0.23")
 
     def test_package_optional_pinning(self):