from __future__ import with_statement import json import logging import os import sys import textwrap from os.path import join, normpath from tempfile import mkdtemp import pretend import pytest from pip._internal.req.constructors import install_req_from_line from pip._internal.utils.misc import rmtree from tests.lib import assert_all_changes, create_test_package_with_setup, need_svn from tests.lib.local_repos import local_checkout, local_repo @pytest.mark.network def test_basic_uninstall(script): """ Test basic install and uninstall. """ result = script.pip('install', 'INITools==0.2') result.did_create(join(script.site_packages, 'initools')) # the import forces the generation of __pycache__ if the version of python # supports it script.run('python', '-c', "import initools") result2 = script.pip('uninstall', 'INITools', '-y') assert_all_changes(result, result2, [script.venv / 'build', 'cache']) def test_basic_uninstall_distutils(script): """ Test basic install and uninstall. """ script.scratch_path.joinpath("distutils_install").mkdir() pkg_path = script.scratch_path / 'distutils_install' pkg_path.joinpath("setup.py").write_text(textwrap.dedent(""" from distutils.core import setup setup( name='distutils-install', version='0.1', ) """)) result = script.run('python', pkg_path / 'setup.py', 'install') result = script.pip('list', '--format=json') assert {"name": "distutils-install", "version": "0.1"} \ in json.loads(result.stdout) result = script.pip('uninstall', 'distutils_install', '-y', expect_stderr=True, expect_error=True) assert ( "Cannot uninstall 'distutils-install'. It is a distutils installed " "project and thus we cannot accurately determine which files belong " "to it which would lead to only a partial uninstall." ) in result.stderr @pytest.mark.network def test_basic_uninstall_with_scripts(script): """ Uninstall an easy_installed package with scripts. """ result = script.easy_install('PyLogo', expect_stderr=True) easy_install_pth = script.site_packages / 'easy-install.pth' pylogo = sys.platform == 'win32' and 'pylogo' or 'PyLogo' assert(pylogo in result.files_updated[easy_install_pth].bytes) result2 = script.pip('uninstall', 'pylogo', '-y') assert_all_changes( result, result2, [script.venv / 'build', 'cache', easy_install_pth], ) @pytest.mark.network def test_uninstall_easy_install_after_import(script): """ Uninstall an easy_installed package after it's been imported """ result = script.easy_install('INITools==0.2', expect_stderr=True) # the import forces the generation of __pycache__ if the version of python # supports it script.run('python', '-c', "import initools") result2 = script.pip('uninstall', 'INITools', '-y') assert_all_changes( result, result2, [ script.venv / 'build', 'cache', script.site_packages / 'easy-install.pth', ] ) @pytest.mark.network def test_uninstall_trailing_newline(script): """ Uninstall behaves appropriately if easy-install.pth lacks a trailing newline """ script.easy_install('INITools==0.2', expect_stderr=True) script.easy_install('PyLogo', expect_stderr=True) easy_install_pth = script.site_packages_path / 'easy-install.pth' # trim trailing newline from easy-install.pth with open(easy_install_pth) as f: pth_before = f.read() with open(easy_install_pth, 'w') as f: f.write(pth_before.rstrip()) # uninstall initools script.pip('uninstall', 'INITools', '-y') with open(easy_install_pth) as f: pth_after = f.read() # verify that only initools is removed before_without_initools = [ line for line in pth_before.splitlines() if 'initools' not in line.lower() ] lines_after = pth_after.splitlines() assert lines_after == before_without_initools @pytest.mark.network def test_basic_uninstall_namespace_package(script): """ Uninstall a distribution with a namespace package without clobbering the namespace and everything in it. """ result = script.pip('install', 'pd.requires==0.0.3') result.did_create(join(script.site_packages, 'pd')) result2 = script.pip('uninstall', 'pd.find', '-y') assert join(script.site_packages, 'pd') not in result2.files_deleted, ( sorted(result2.files_deleted.keys()) ) assert join(script.site_packages, 'pd', 'find') in result2.files_deleted, ( sorted(result2.files_deleted.keys()) ) def test_uninstall_overlapping_package(script, data): """ 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 = data.packages.joinpath("parent-0.1.tar.gz") child_pkg = data.packages.joinpath("child-0.1.tar.gz") result1 = script.pip('install', parent_pkg) result1.did_create(join(script.site_packages, 'parent')) result2 = script.pip('install', child_pkg) result2.did_create(join(script.site_packages, 'child')) result2.did_create(normpath( join(script.site_packages, 'parent/plugins/child_plugin.py') )) # The import forces the generation of __pycache__ if the version of python # supports it script.run('python', '-c', "import parent.plugins.child_plugin, child") result3 = script.pip('uninstall', '-y', 'child') assert join(script.site_packages, 'child') in result3.files_deleted, ( sorted(result3.files_created.keys()) ) assert normpath( join(script.site_packages, 'parent/plugins/child_plugin.py') ) in result3.files_deleted, sorted(result3.files_deleted.keys()) assert join(script.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, []) @pytest.mark.parametrize("console_scripts", ["test_ = distutils_install", "test_:test_ = distutils_install"]) def test_uninstall_entry_point_colon_in_name(script, console_scripts): """ Test uninstall package with two or more entry points in the same section, whose name contain a colon. """ pkg_name = 'ep_install' pkg_path = create_test_package_with_setup( script, name=pkg_name, version='0.1', entry_points={"console_scripts": [console_scripts, ], "pip_test.ep": ["ep:name1 = distutils_install", "ep:name2 = distutils_install"] } ) script_name = script.bin_path.joinpath( console_scripts.split('=')[0].strip() ) if sys.platform == 'win32': script_name += '.exe' result = script.pip('install', pkg_path) assert script_name.exists() result = script.pip('list', '--format=json') assert {"name": "ep-install", "version": "0.1"} \ in json.loads(result.stdout) script.pip('uninstall', 'ep_install', '-y') assert not script_name.exists() result2 = script.pip('list', '--format=json') assert {"name": "ep-install", "version": "0.1"} \ not in json.loads(result2.stdout) def test_uninstall_gui_scripts(script): """ Make sure that uninstall removes gui scripts """ pkg_name = "gui_pkg" pkg_path = create_test_package_with_setup( script, name=pkg_name, version='0.1', entry_points={"gui_scripts": ["test_ = distutils_install", ], } ) script_name = script.bin_path.joinpath('test_') if sys.platform == 'win32': script_name += '.exe' script.pip('install', pkg_path) assert script_name.exists() script.pip('uninstall', pkg_name, '-y') assert not script_name.exists() def test_uninstall_console_scripts(script): """ Test uninstalling a package with more files (console_script entry points, extra directories). """ pkg_path = create_test_package_with_setup( script, name='discover', version='0.1', entry_points={'console_scripts': ['discover = discover:main']}, ) result = script.pip('install', pkg_path) result.did_create(script.bin / 'discover' + script.exe) result2 = script.pip('uninstall', 'discover', '-y') assert_all_changes(result, result2, [script.venv / 'build', 'cache']) def test_uninstall_console_scripts_uppercase_name(script): """ Test uninstalling console script with uppercase character. """ pkg_path = create_test_package_with_setup( script, name='ep_install', version='0.1', entry_points={ "console_scripts": [ "Test = distutils_install", ], }, ) script_name = script.bin_path.joinpath('Test' + script.exe) script.pip('install', pkg_path) assert script_name.exists() script.pip('uninstall', 'ep_install', '-y') assert not script_name.exists() @pytest.mark.network def test_uninstall_easy_installed_console_scripts(script): """ Test uninstalling package with console_scripts that is easy_installed. """ # setuptools >= 42.0.0 deprecates easy_install and prints a warning when # used result = script.easy_install('discover', allow_stderr_warning=True) result.did_create(script.bin / 'discover' + script.exe) result2 = script.pip('uninstall', 'discover', '-y') assert_all_changes( result, result2, [ script.venv / 'build', 'cache', script.site_packages / 'easy-install.pth', ] ) @pytest.mark.xfail @pytest.mark.network @need_svn def test_uninstall_editable_from_svn(script, tmpdir): """ Test uninstalling an editable installation from svn. """ result = script.pip( 'install', '-e', '{checkout}#egg=initools'.format( checkout=local_checkout( 'svn+http://svn.colorstudy.com/INITools', tmpdir) ), ) result.assert_installed('INITools') result2 = script.pip('uninstall', '-y', 'initools') assert (script.venv / 'src' / 'initools' in result2.files_after) assert_all_changes( result, result2, [ script.venv / 'src', script.venv / 'build', script.site_packages / 'easy-install.pth' ], ) @pytest.mark.network def test_uninstall_editable_with_source_outside_venv(script, tmpdir): """ Test uninstalling editable install from existing source outside the venv. """ try: temp = mkdtemp() temp_pkg_dir = join(temp, 'pip-test-package') _test_uninstall_editable_with_source_outside_venv( script, tmpdir, temp_pkg_dir, ) finally: rmtree(temp) def _test_uninstall_editable_with_source_outside_venv( script, tmpdir, temp_pkg_dir, ): result = script.run( 'git', 'clone', local_repo('git+git://github.com/pypa/pip-test-package', tmpdir), temp_pkg_dir, expect_stderr=True, ) result2 = script.pip('install', '-e', temp_pkg_dir) result2.did_create(join( script.site_packages, 'pip-test-package.egg-link' )) result3 = script.pip('uninstall', '-y', 'pip-test-package') assert_all_changes( result, result3, [script.venv / 'build', script.site_packages / 'easy-install.pth'], ) @pytest.mark.xfail @pytest.mark.network @need_svn def test_uninstall_from_reqs_file(script, tmpdir): """ Test uninstall from a requirements file. """ local_svn_url = local_checkout( 'svn+http://svn.colorstudy.com/INITools', tmpdir, ) script.scratch_path.joinpath("test-req.txt").write_text( textwrap.dedent(""" -e {url}#egg=initools # and something else to test out: PyLogo<0.4 """).format(url=local_svn_url) ) result = script.pip('install', '-r', 'test-req.txt') script.scratch_path.joinpath("test-req.txt").write_text( textwrap.dedent(""" # -f, -i, and --extra-index-url should all be ignored by uninstall -f http://www.example.com -i http://www.example.com --extra-index-url http://www.example.com -e {url}#egg=initools # and something else to test out: PyLogo<0.4 """).format(url=local_svn_url) ) result2 = script.pip('uninstall', '-r', 'test-req.txt', '-y') assert_all_changes( result, result2, [ script.venv / 'build', script.venv / 'src', script.scratch / 'test-req.txt', script.site_packages / 'easy-install.pth', ], ) def test_uninstallpathset_no_paths(caplog): """ Test UninstallPathSet logs notification when there are no paths to uninstall """ from pkg_resources import get_distribution from pip._internal.req.req_uninstall import UninstallPathSet caplog.set_level(logging.INFO) test_dist = get_distribution('pip') uninstall_set = UninstallPathSet(test_dist) uninstall_set.remove() # with no files added to set assert ( "Can't uninstall 'pip'. No files were found to uninstall." in caplog.text ) def test_uninstall_non_local_distutils(caplog, monkeypatch, tmpdir): einfo = tmpdir.joinpath("thing-1.0.egg-info") with open(einfo, "wb"): pass dist = pretend.stub( key="thing", project_name="thing", egg_info=einfo, location=einfo, _provider=pretend.stub(), ) get_dist = pretend.call_recorder(lambda x: dist) monkeypatch.setattr("pip._vendor.pkg_resources.get_distribution", get_dist) req = install_req_from_line("thing") req.uninstall() assert os.path.exists(einfo) def test_uninstall_wheel(script, data): """ Test uninstalling a wheel """ package = data.packages.joinpath("simple.dist-0.1-py2.py3-none-any.whl") result = script.pip('install', package, '--no-index') dist_info_folder = script.site_packages / 'simple.dist-0.1.dist-info' result.did_create(dist_info_folder) result2 = script.pip('uninstall', 'simple.dist', '-y') assert_all_changes(result, result2, []) @pytest.mark.skipif("sys.platform == 'win32'") def test_uninstall_with_symlink(script, data, tmpdir): """ Test uninstalling a wheel, with an additional symlink https://github.com/pypa/pip/issues/6892 """ package = data.packages.joinpath("simple.dist-0.1-py2.py3-none-any.whl") script.pip('install', package, '--no-index') symlink_target = tmpdir / "target" symlink_target.mkdir() symlink_source = script.site_packages / "symlink" (script.base_path / symlink_source).symlink_to(symlink_target) st_mode = symlink_target.stat().st_mode distinfo_path = script.site_packages_path / 'simple.dist-0.1.dist-info' record_path = distinfo_path / 'RECORD' with open(record_path, "a") as f: f.write("symlink,,\n") uninstall_result = script.pip('uninstall', 'simple.dist', '-y') assert symlink_source in uninstall_result.files_deleted assert symlink_target.stat().st_mode == st_mode def test_uninstall_setuptools_develop_install(script, data): """Try uninstall after setup.py develop followed of setup.py install""" pkg_path = data.packages.joinpath("FSPkg") script.run('python', 'setup.py', 'develop', expect_stderr=True, cwd=pkg_path) script.run('python', 'setup.py', 'install', expect_stderr=True, cwd=pkg_path) list_result = script.pip('list', '--format=json') assert {"name": os.path.normcase("FSPkg"), "version": "0.1.dev0"} \ in json.loads(list_result.stdout), str(list_result) # Uninstall both develop and install uninstall = script.pip('uninstall', 'FSPkg', '-y') assert any(filename.endswith('.egg') for filename in uninstall.files_deleted.keys()) uninstall2 = script.pip('uninstall', 'FSPkg', '-y') assert join( script.site_packages, 'FSPkg.egg-link' ) in uninstall2.files_deleted, list(uninstall2.files_deleted.keys()) list_result2 = script.pip('list', '--format=json') assert "FSPkg" not in {p["name"] for p in json.loads(list_result2.stdout)} def test_uninstall_editable_and_pip_install(script, data): """Try uninstall after pip install -e after pip install""" # SETUPTOOLS_SYS_PATH_TECHNIQUE=raw removes the assumption that `-e` # installs are always higher priority than regular installs. # This becomes the default behavior in setuptools 25. script.environ['SETUPTOOLS_SYS_PATH_TECHNIQUE'] = 'raw' pkg_path = data.packages.joinpath("FSPkg") script.pip('install', '-e', '.', expect_stderr=True, cwd=pkg_path) # ensure both are installed with --ignore-installed: script.pip('install', '--ignore-installed', '.', expect_stderr=True, cwd=pkg_path) list_result = script.pip('list', '--format=json') assert {"name": "FSPkg", "version": "0.1.dev0"} \ in json.loads(list_result.stdout) # Uninstall both develop and install uninstall = script.pip('uninstall', 'FSPkg', '-y') assert not any(filename.endswith('.egg-link') for filename in uninstall.files_deleted.keys()) uninstall2 = script.pip('uninstall', 'FSPkg', '-y') assert join( script.site_packages, 'FSPkg.egg-link' ) in uninstall2.files_deleted, list(uninstall2.files_deleted.keys()) list_result2 = script.pip('list', '--format=json') assert "FSPkg" not in {p["name"] for p in json.loads(list_result2.stdout)} def test_uninstall_editable_and_pip_install_easy_install_remove(script, data): """Try uninstall after pip install -e after pip install and removing easy-install.pth""" # SETUPTOOLS_SYS_PATH_TECHNIQUE=raw removes the assumption that `-e` # installs are always higher priority than regular installs. # This becomes the default behavior in setuptools 25. script.environ['SETUPTOOLS_SYS_PATH_TECHNIQUE'] = 'raw' # Rename easy-install.pth to pip-test.pth easy_install_pth = join(script.site_packages_path, 'easy-install.pth') pip_test_pth = join(script.site_packages_path, 'pip-test.pth') os.rename(easy_install_pth, pip_test_pth) # Install FSPkg pkg_path = data.packages.joinpath("FSPkg") script.pip('install', '-e', '.', expect_stderr=True, cwd=pkg_path) # Rename easy-install.pth to pip-test-fspkg.pth pip_test_fspkg_pth = join(script.site_packages_path, 'pip-test-fspkg.pth') os.rename(easy_install_pth, pip_test_fspkg_pth) # Confirm that FSPkg is installed list_result = script.pip('list', '--format=json') assert {"name": "FSPkg", "version": "0.1.dev0"} \ in json.loads(list_result.stdout) # Remove pip-test-fspkg.pth os.remove(pip_test_fspkg_pth) # Uninstall will fail with given warning uninstall = script.pip('uninstall', 'FSPkg', '-y') assert "Cannot remove entries from nonexistent file" in uninstall.stderr assert join( script.site_packages, 'FSPkg.egg-link' ) in uninstall.files_deleted, list(uninstall.files_deleted.keys()) # Confirm that FSPkg is uninstalled list_result = script.pip('list', '--format=json') assert {"name": "FSPkg", "version": "0.1.dev0"} \ not in json.loads(list_result.stdout) # Rename pip-test.pth back to easy-install.pth os.rename(pip_test_pth, easy_install_pth) def test_uninstall_ignores_missing_packages(script, data): """Uninstall of a non existent package prints a warning and exits cleanly """ result = script.pip( 'uninstall', '-y', 'non-existent-pkg', expect_stderr=True, ) assert "Skipping non-existent-pkg as it is not installed." in result.stderr assert result.returncode == 0, "Expected clean exit" def test_uninstall_ignores_missing_packages_and_uninstalls_rest(script, data): script.pip_install_local('simple') result = script.pip( 'uninstall', '-y', 'non-existent-pkg', 'simple', expect_stderr=True, ) assert "Skipping non-existent-pkg as it is not installed." in result.stderr assert "Successfully uninstalled simple" in result.stdout assert result.returncode == 0, "Expected clean exit"