diff --git a/pip/req.py b/pip/req.py index 8b7bd06cb77..33018a59085 100644 --- a/pip/req.py +++ b/pip/req.py @@ -433,7 +433,7 @@ def uninstall(self, auto_confirm=False): for installed_file in dist.get_metadata('installed-files.txt').splitlines(): path = os.path.normpath(os.path.join(egg_info_path, installed_file)) paths_to_remove.add(path) - if dist.has_metadata('top_level.txt'): + elif dist.has_metadata('top_level.txt'): if dist.has_metadata('namespace_packages.txt'): namespaces = dist.get_metadata('namespace_packages.txt') else: diff --git a/tests/packages/README.txt b/tests/packages/README.txt index b6ecde635bf..069b32c1437 100644 --- a/tests/packages/README.txt +++ b/tests/packages/README.txt @@ -4,3 +4,6 @@ Version 0.2broken has a setup.py crafted to fail on install (and only on install). If any earlier step would fail (i.e. egg-info-generation), the already-installed version would never be uninstalled, so uninstall-rollback would not come into play. + +The parent-0.1.tar.gz and child-0.1.tar.gz packages are used by +test_uninstall:test_uninstall_overlapping_package. diff --git a/tests/packages/child-0.1.tar.gz b/tests/packages/child-0.1.tar.gz new file mode 100644 index 00000000000..2fb34ca5d61 Binary files /dev/null and b/tests/packages/child-0.1.tar.gz differ diff --git a/tests/packages/parent-0.1.tar.gz b/tests/packages/parent-0.1.tar.gz new file mode 100644 index 00000000000..7d8673cacaa Binary files /dev/null and b/tests/packages/parent-0.1.tar.gz differ diff --git a/tests/test_uninstall.py b/tests/test_uninstall.py index c88c7a681f0..8713d9cb8b3 100644 --- a/tests/test_uninstall.py +++ b/tests/test_uninstall.py @@ -1,8 +1,9 @@ import textwrap import sys -from os.path import join +from os.path import abspath, join from tempfile import mkdtemp -from tests.test_pip import reset_env, run_pip, assert_all_changes, write_file +from tests.test_pip import (here, reset_env, run_pip, assert_all_changes, + write_file) from tests.local_repos import local_repo, local_checkout from pip.util import rmtree @@ -48,6 +49,31 @@ def test_uninstall_namespace_package(): assert join(env.site_packages, 'pd', 'find') in result2.files_deleted, sorted(result2.files_deleted.keys()) +def test_uninstall_overlapping_package(): + """ + Uninstalling a distribution that adds modules to a pre-existing package + should only remove those added modules, not the rest of the existing + package. + + See: GitHub issue #355 (pip uninstall removes things it didn't install) + """ + parent_pkg = abspath(join(here, 'packages', 'parent-0.1.tar.gz')) + child_pkg = abspath(join(here, 'packages', 'child-0.1.tar.gz')) + env = reset_env() + result1 = run_pip('install', parent_pkg, expect_error=False) + assert join(env.site_packages, 'parent') in result1.files_created, sorted(result1.files_created.keys()) + result2 = run_pip('install', child_pkg, expect_error=False) + assert join(env.site_packages, 'child') in result2.files_created, sorted(result2.files_created.keys()) + assert join(env.site_packages, 'parent/plugins/child_plugin.py') in result2.files_created, sorted(result2.files_created.keys()) + result3 = run_pip('uninstall', '-y', 'child', expect_error=False) + assert join(env.site_packages, 'child') in result3.files_deleted, sorted(result3.files_created.keys()) + assert join(env.site_packages, 'parent/plugins/child_plugin.py') in result3.files_deleted, sorted(result3.files_deleted.keys()) + assert join(env.site_packages, 'parent') not in result3.files_deleted, sorted(result3.files_deleted.keys()) + # Additional check: uninstalling 'child' should return things to the + # previous state, without unintended side effects. + assert_all_changes(result2, result3, []) + + def test_uninstall_console_scripts(): """ Test uninstalling a package with more files (console_script entry points, extra directories).