Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

confirming dependencies when uninstall package #4245

Closed
wants to merge 23 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions news/4245.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
added confirming dependencies to ``uninstall`` command.
3 changes: 3 additions & 0 deletions src/pip/_internal/commands/uninstall.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from pip._internal.basecommand import Command
from pip._internal.exceptions import InstallationError
from pip._internal.req import InstallRequirement, parse_requirements
from pip._internal.utils.misc import confirm_dependencies
from pip._internal.utils.misc import protect_pip_from_modification_on_windows


Expand Down Expand Up @@ -65,6 +66,8 @@ def run(self, options, args):
'"pip help %(name)s")' % dict(name=self.name)
)

if not confirm_dependencies(reqs_to_uninstall):
return
protect_pip_from_modification_on_windows(
modifying_pip="pip" in reqs_to_uninstall
)
Expand Down
29 changes: 29 additions & 0 deletions src/pip/_internal/utils/misc.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
from collections import deque

from pip._vendor import pkg_resources
from pip._vendor.packaging.utils import canonicalize_name
# NOTE: retrying is not annotated in typeshed as on 2017-07-17, which is
# why we ignore the type on this import.
from pip._vendor.retrying import retry # type: ignore
Expand Down Expand Up @@ -854,6 +855,34 @@ def enum(*sequential, **named):
return type('Enum', (), enums)


def confirm_dependencies(reqs_to_uninstall):
depends = list(get_depends(reqs_to_uninstall))
if not depends:
return True

for d, deps in depends:
msg = ("The following packages depend on "
"%s and may break if it is uninstalled:") % d
logger.info(msg)
for dep in deps:
logger.info(" " + dep)

return ask("uninstall? (y/n)", options=('y', 'n')) == 'y'


def get_depends(reqs_to_uninstall):
dist_deps = {d.key: [canonicalize_name(r.name) for r in d.requires()]
for d in get_installed_distributions()}
logger.debug('dist_deps %s', dist_deps)
dependants = {req: [d for d, r in dist_deps.items() if req in r]
for req in reqs_to_uninstall}
logger.debug('dependants %s', dependants)
for d, deps in dependants.items():
deps = [r for r in deps if r not in reqs_to_uninstall]
if deps:
yield d, deps


def remove_auth_from_url(url):
# Return a copy of url with 'username:password@' removed.
# username/pass params are passed to subversion through flags
Expand Down
Binary file not shown.
Empty file.
2 changes: 2 additions & 0 deletions tests/functional/dummy1/setup.cfg
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
[bdist_wheel]
universal = 1
6 changes: 6 additions & 0 deletions tests/functional/dummy1/setup.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
from setuptools import setup
setup(
name="dummy1",
install_requires=["dummy2"],
packages=["dummy1"],
)
Binary file not shown.
Empty file.
2 changes: 2 additions & 0 deletions tests/functional/dummy2/setup.cfg
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
[bdist_wheel]
universal = 1
6 changes: 6 additions & 0 deletions tests/functional/dummy2/setup.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
from setuptools import setup
setup(
name="dummy2",
install_requires=["dummy1"],
packages=["dummy2"],
)
86 changes: 85 additions & 1 deletion tests/functional/test_uninstall.py
Original file line number Diff line number Diff line change
Expand Up @@ -144,13 +144,97 @@ def test_basic_uninstall_namespace_package(script):
assert join(script.site_packages, 'pd') in result.files_created, (
sorted(result.files_created.keys())
)
result2 = script.pip('uninstall', 'pd.find', '-y', expect_error=True)
result2 = script.pip('uninstall', 'pd.requires', '-y', expect_error=True)
assert join(script.site_packages, 'pd') not in result2.files_deleted, (
sorted(result2.files_deleted.keys())
)
pd_requires_path = join(script.site_packages, 'pd', 'requires')
assert pd_requires_path in result2.files_deleted, (
sorted(result2.files_deleted.keys())
)


@pytest.mark.network
def test_uninstall_dependency(script):
"""
Uninstall a distribution depended from other, answer `y` to confirmation.

"""
result = script.pip('install', 'pd.requires==0.0.3', expect_error=True)
assert join(script.site_packages, 'pd') in result.files_created, (
sorted(result.files_created.keys())
)
result2 = script.pip('uninstall', 'pd.find', '-y',
expect_error=True, stdin=b"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())
)


@pytest.mark.network
def test_uninstall_dependency_no(script):
"""
Not uninstalling, answer `no` to confirmation.

"""
result = script.pip('install', 'pd.requires==0.0.3', expect_error=True)
assert join(script.site_packages, 'pd') in result.files_created, (
sorted(result.files_created.keys())
)
result2 = script.pip('uninstall', 'pd.find', '-y',
expect_error=True, stdin=b"n")
assert not result2.files_deleted


@pytest.mark.network
def test_uninstall_dependency_all(script):
"""
Uninstall all dependencies, no confirmation.
"""
result = script.pip('install', 'pd.requires==0.0.3', expect_error=True)
assert join(script.site_packages, 'pd') in result.files_created, (
sorted(result.files_created.keys())
)
result2 = script.pip('uninstall', 'pd.requires', 'pd.find', '-y',
expect_error=True, stdin=b"y")
assert join(script.site_packages, 'pd', 'find') in result2.files_deleted, (
sorted(result2.files_deleted.keys())
)
pd_requires_path = join(script.site_packages, 'pd', 'requires')
assert pd_requires_path in result2.files_deleted, (
sorted(result2.files_deleted.keys())
)
assert join(script.site_packages, 'pd') in result2.files_deleted, (
sorted(result2.files_deleted.keys())
)


@pytest.mark.network
def test_uninstall_recursive_dependencies(script):
"""
Uninstall recursive dependencies.
"""
here = os.path.normpath(os.path.abspath(os.path.dirname(__file__)))
dummy1_whl = os.path.join(here, "dummy1-0.0.0-py2.py3-none-any.whl")
dummy2_whl = os.path.join(here, "dummy2-0.0.0-py2.py3-none-any.whl")
result = script.pip('install', dummy1_whl, dummy2_whl, expect_error=True)
assert join(script.site_packages, 'dummy1') in result.files_created, (
sorted(result.files_created.keys())
)
assert join(script.site_packages, 'dummy2') in result.files_created, (
sorted(result.files_created.keys())
)
result2 = script.pip('uninstall', 'dummy1', 'dummy2', '-y',
expect_error=True, stdin=b"y")
assert join(script.site_packages, 'dummy1') in result2.files_deleted, (
sorted(result2.files_deleted.keys())
)
assert join(script.site_packages, 'dummy2') in result2.files_deleted, (
sorted(result2.files_deleted.keys())
)


def test_uninstall_overlapping_package(script, data):
Expand Down
31 changes: 31 additions & 0 deletions tests/unit/test_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,13 @@

import pytest
from mock import Mock, patch
from pip._vendor.packaging.requirements import Requirement
from pip._vendor.six import BytesIO

from pip._internal.exceptions import (
HashMismatch, HashMissing, InstallationError, UnsupportedPythonVersion,
)
from pip._internal.req.req_install import InstallRequirement
from pip._internal.utils.encoding import auto_decode
from pip._internal.utils.glibc import check_glibc_version
from pip._internal.utils.hashes import Hashes, MissingHashes
Expand Down Expand Up @@ -595,6 +597,35 @@ def test_check_requires(self, metadata, should_raise):
check_dist_requires_python(fake_dist)


@patch('pip._internal.utils.misc.ask')
@patch('pip._internal.utils.misc.get_installed_distributions')
def test_confirm_dependencies(mock_gid, mock_ask):
from pip._internal.utils.misc import confirm_dependencies

class installed:
def __init__(self, key, requires):
self.key = key
self._requires = requires

def requires(self):
return self._requires

class installed_require:
def __init__(self, name):
self.name = name

installed_packages = [
installed('dependant',
[InstallRequirement(Requirement("dummy"), None)]),
]
mock_gid.return_value = installed_packages
mock_ask.return_value = 'y'

reqs_to_uninstall = ["dummy"]
result = confirm_dependencies(reqs_to_uninstall)
assert result


class TestGetProg(object):

@pytest.mark.parametrize(
Expand Down