Repository: conda/conda

Issue:
Respecting topological sorting for environment removal
Not sure if this is implemented in newer versions of `conda`, but it would be nice if environment removal performed a topological sort and uninstalled packages based on this sorting. Namely only removing packages that are not required for anything else first. For the most part this doesn't matter, but when `pre-unlink` scripts are involved it does.


Discussion:
Also ran into a similar issue when doing `conda update --all` recently.
I *think* this should be done in 4.4.  I'm adding the milestone so I can confirm later.  If it's not done yet, it might get bumped past 4.4.0.  Hopefully it's already done.
That's great news! Thanks @kalefranz. 😄

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,15 +1,14 @@
 # -*- 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
+from .linked_data import PrefixData
 from .._vendor.boltons.setutils import IndexedSet
 from ..base.context import context
 from ..common.compat import iteritems, itervalues, odict, string_types, text_type
@@ -380,7 +379,11 @@ def solve_for_diff(self, deps_modifier=NULL, prune=NULL, ignore_pinned=NULL,
 
         """
         final_precs = self.solve_final_state(deps_modifier, prune, ignore_pinned, force_remove)
-        previous_records = IndexedSet(itervalues(linked_data(self.prefix)))
+        previous_records = IndexedSet(self._index[d] for d in self._r.dependency_sort(
+            {prefix_rec.name: Dist(prefix_rec)
+             for prefix_rec in PrefixData(self.prefix).iter_records()}
+        ))
+
         unlink_precs = previous_records - final_precs
         link_precs = final_precs - previous_records
 
@@ -397,6 +400,9 @@ def solve_for_diff(self, deps_modifier=NULL, prune=NULL, ignore_pinned=NULL,
         # TODO: force_reinstall might not yet be fully implemented in :meth:`solve_final_state`,
         #       at least as described in the docstring.
 
+        unlink_precs = IndexedSet(reversed(sorted(unlink_precs,
+                                                  key=lambda x: previous_records.index(x))))
+        link_precs = IndexedSet(sorted(link_precs, key=lambda x: final_precs.index(x)))
         return unlink_precs, link_precs
 
     def solve_for_transaction(self, deps_modifier=NULL, prune=NULL, ignore_pinned=NULL,


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
@@ -170,49 +170,49 @@ def test_prune_1():
     specs_to_remove = MatchSpec("numbapro"),
     with get_solver(specs_to_remove=specs_to_remove, prefix_records=final_state_1,
                     history_specs=specs) as solver:
-        final_state_2 = solver.solve_final_state(prune=False)
+        unlink_precs, link_precs = solver.solve_for_diff(prune=False)
         # PrefixDag(final_state_2, specs).open_url()
-        print([Dist(rec).full_name for rec in final_state_2])
-        order = (
-            'channel-1::libnvvm-1.0-p0',
-            '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::zlib-1.2.7-0',
-            'channel-1::llvm-3.2-0',
-            'channel-1::mkl-rt-11.0-p0',
-            'channel-1::python-2.7.3-7',
-            'channel-1::bitarray-0.8.1-py27_0',
-            'channel-1::llvmpy-0.11.2-py27_0',
-            'channel-1::meta-0.4.2.dev-py27_0',
-            'channel-1::mkl-service-1.0.0-py27_p0',
-            'channel-1::numpy-1.6.2-py27_p4',
-            'channel-1::numba-0.8.1-np16py27_0',
-            'channel-1::numexpr-2.1-np16py27_p0',
-            'channel-1::scipy-0.12.0-np16py27_p0',
-            'channel-1::scikit-learn-0.13.1-np16py27_p0',
-            'channel-1::mkl-11.0-np16py27_p0',
+        print([Dist(rec).full_name for rec in unlink_precs])
+        unlink_order = (
+            'channel-1::accelerate-1.1.0-np16py27_p0',
+            'channel-1::numbapro-0.11.0-np16py27_p0',
         )
-        assert tuple(final_state_2) == tuple(solver._index[Dist(d)] for d in order)
+        assert tuple(unlink_precs) == tuple(solver._index[Dist(d)] for d in unlink_order)
+
+        print([Dist(rec).full_name for rec in link_precs])
+        link_order = ()
+        assert tuple(link_precs) == tuple(solver._index[Dist(d)] for d in link_order)
 
     with get_solver(specs_to_remove=specs_to_remove, prefix_records=final_state_1,
                     history_specs=specs) as solver:
-        final_state_2 = solver.solve_final_state(prune=True)
+        unlink_precs, link_precs = solver.solve_for_diff(prune=True)
         # 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::zlib-1.2.7-0',
-            'channel-1::python-2.7.3-7',
+        print([Dist(rec).full_name for rec in unlink_precs])
+        unlink_order = (
+            'channel-1::accelerate-1.1.0-np16py27_p0',
+            'channel-1::mkl-11.0-np16py27_p0',
+            'channel-1::scikit-learn-0.13.1-np16py27_p0',
+            'channel-1::numbapro-0.11.0-np16py27_p0',
+            'channel-1::scipy-0.12.0-np16py27_p0',
+            'channel-1::numexpr-2.1-np16py27_p0',
+            'channel-1::numba-0.8.1-np16py27_0',
+            'channel-1::numpy-1.6.2-py27_p4',
+            'channel-1::mkl-service-1.0.0-py27_p0',
+            'channel-1::meta-0.4.2.dev-py27_0',
+            'channel-1::llvmpy-0.11.2-py27_0',
+            'channel-1::bitarray-0.8.1-py27_0',
+            'channel-1::mkl-rt-11.0-p0',
+            'channel-1::llvm-3.2-0',
+            'channel-1::libnvvm-1.0-p0',
+        )
+        assert tuple(unlink_precs) == tuple(solver._index[Dist(d)] for d in unlink_order)
+
+        print([Dist(rec).full_name for rec in link_precs])
+        link_order = (
             'channel-1::numpy-1.6.2-py27_4',
         )
-        assert tuple(final_state_2) == tuple(solver._index[Dist(d)] for d in order)
+        assert tuple(link_precs) == tuple(solver._index[Dist(d)] for d in link_order)
+
 
 
 def test_force_remove_1():