pip/tests/functional/test_uninstall.py

609 lines
20 KiB
Python
Raw Normal View History

from __future__ import with_statement
import json
import logging
2013-01-23 05:52:35 +01:00
import os
import sys
2017-05-16 12:16:30 +02:00
import textwrap
from os.path import join, normpath
2010-02-24 11:24:55 +01:00
from tempfile import mkdtemp
2017-05-16 12:16:30 +02:00
import pretend
import pytest
from pip._internal.req.constructors import install_req_from_line
from pip._internal.utils.misc import rmtree
2020-09-23 16:27:09 +02:00
from tests.lib import assert_all_changes, create_test_package_with_setup, need_svn
2017-05-16 12:16:30 +02:00
from tests.lib.local_repos import local_checkout, local_repo
2015-01-15 00:53:15 +01:00
@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'])
2010-02-24 11:24:55 +01:00
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)
2017-03-21 12:28:22 +01:00
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."
2017-03-21 12:28:22 +01:00
) in result.stderr
2015-01-15 00:53:15 +01:00
@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],
)
2010-02-24 11:24:55 +01:00
2015-01-15 00:53:15 +01:00
@pytest.mark.network
def test_uninstall_easy_install_after_import(script):
2012-08-19 00:26:17 +02:00
"""
Uninstall an easy_installed package after it's been imported
"""
2020-06-24 18:06:15 +02:00
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',
]
)
2012-08-19 00:26:17 +02:00
@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
2015-01-15 00:53:15 +01:00
@pytest.mark.network
def test_basic_uninstall_namespace_package(script):
"""
2010-02-24 11:24:55 +01:00
Uninstall a distribution with a namespace package without clobbering
the namespace and everything in it.
"""
2019-08-11 04:04:44 +02:00
result = script.pip('install', 'pd.requires==0.0.3')
result.did_create(join(script.site_packages, 'pd'))
2019-08-11 04:04:44 +02:00
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())
)
2010-02-24 11:24:55 +01:00
def test_uninstall_overlapping_package(script, data):
2012-07-09 09:04:46 +02:00
"""
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())
)
2012-07-09 09:04:46 +02:00
# 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)
2016-02-01 23:17:11 +01:00
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)
2019-08-11 04:04:44 +02:00
result2 = script.pip('uninstall', 'discover', '-y')
assert_all_changes(result, result2, [script.venv / 'build', 'cache'])
2010-02-24 11:24:55 +01:00
2020-01-10 13:09:47 +01:00
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()
2015-01-15 00:53:15 +01:00
@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',
]
)
2010-02-24 11:24:55 +01:00
@pytest.mark.xfail
2015-01-15 00:53:15 +01:00
@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'
],
)
2010-02-24 11:24:55 +01:00
2015-01-15 00:53:15 +01:00
@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'
))
2019-08-11 04:04:44 +02:00
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
2015-01-15 00:53:15 +01:00
@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',
],
)
2012-05-22 12:54:40 +02:00
def test_uninstallpathset_no_paths(caplog):
"""
Test UninstallPathSet logs notification when there are no paths to
uninstall
"""
2012-09-03 19:55:29 +02:00
from pkg_resources import get_distribution
2020-09-23 15:08:01 +02:00
from pip._internal.req.req_uninstall import UninstallPathSet
caplog.set_level(logging.INFO)
2012-09-03 19:55:29 +02:00
test_dist = get_distribution('pip')
2016-01-20 21:25:51 +01:00
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
)
2012-09-03 19:55:29 +02:00
2012-09-04 21:40:50 +02:00
2016-01-20 21:25:51 +01:00
def test_uninstall_non_local_distutils(caplog, monkeypatch, tmpdir):
einfo = tmpdir.joinpath("thing-1.0.egg-info")
2016-01-20 21:25:51 +01:00
with open(einfo, "wb"):
pass
2016-01-20 21:25:51 +01:00
dist = pretend.stub(
key="thing",
project_name="thing",
egg_info=einfo,
location=einfo,
_provider=pretend.stub(),
)
2016-01-20 21:25:51 +01:00
get_dist = pretend.call_recorder(lambda x: dist)
monkeypatch.setattr("pip._vendor.pkg_resources.get_distribution", get_dist)
req = install_req_from_line("thing")
2016-01-20 21:25:51 +01:00
req.uninstall()
assert os.path.exists(einfo)
2014-04-25 23:42:14 +02:00
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, [])
2019-08-23 23:11:12 +02:00
@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")
2019-08-23 23:11:12 +02:00
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"