Skip to content

Commit

Permalink
Merge pull request #913 from georgek/feature/pip-sync-ask-option
Browse files Browse the repository at this point in the history
Add --ask option to pip-sync
  • Loading branch information
atugushev authored Oct 4, 2019
2 parents e1d2212 + 1269d78 commit 34bce45
Show file tree
Hide file tree
Showing 4 changed files with 114 additions and 22 deletions.
8 changes: 8 additions & 0 deletions piptools/scripts/sync.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,12 @@

@click.command()
@click.version_option()
@click.option(
"-a",
"--ask",
is_flag=True,
help="Show what would happen, then ask whether to continue",
)
@click.option(
"-n",
"--dry-run",
Expand Down Expand Up @@ -63,6 +69,7 @@
)
@click.argument("src_files", required=False, type=click.Path(exists=True), nargs=-1)
def cli(
ask,
dry_run,
force,
find_links,
Expand Down Expand Up @@ -137,5 +144,6 @@ def cli(
verbose=(not quiet),
dry_run=dry_run,
install_flags=install_flags,
ask=ask,
)
)
39 changes: 27 additions & 12 deletions piptools/sync.py
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,14 @@ def diff(compiled_requirements, installed_dists):
return (to_install, to_uninstall)


def sync(to_install, to_uninstall, verbose=False, dry_run=False, install_flags=None):
def sync(
to_install,
to_uninstall,
verbose=False,
dry_run=False,
install_flags=None,
ask=False,
):
"""
Install and uninstalls the given sets of modules.
"""
Expand All @@ -157,26 +164,34 @@ def sync(to_install, to_uninstall, verbose=False, dry_run=False, install_flags=N
if not verbose:
pip_flags += ["-q"]

if to_uninstall:
if dry_run:
if ask:
dry_run = True

if dry_run:
if to_uninstall:
click.echo("Would uninstall:")
for pkg in to_uninstall:
click.echo(" {}".format(pkg))
else:

if to_install:
click.echo("Would install:")
for ireq in to_install:
click.echo(" {}".format(format_requirement(ireq)))

if ask and click.confirm("Would you like to proceed with these changes?"):
dry_run = False

if not dry_run:
if to_uninstall:
check_call( # nosec
[sys.executable, "-m", "pip", "uninstall", "-y"]
+ pip_flags
+ sorted(to_uninstall)
)

if to_install:
if install_flags is None:
install_flags = []
if dry_run:
click.echo("Would install:")
for ireq in to_install:
click.echo(" {}".format(format_requirement(ireq)))
else:
if to_install:
if install_flags is None:
install_flags = []
# prepare requirement lines
req_lines = []
for ireq in sorted(to_install, key=key_from_ireq):
Expand Down
27 changes: 27 additions & 0 deletions tests/test_cli_sync.py
Original file line number Diff line number Diff line change
Expand Up @@ -147,3 +147,30 @@ def test_pip_install_flags(check_call, cli_flags, expected_install_flags, runner
assert [args[6:] for args in call_args if args[3] == "install"] == [
expected_install_flags
]


@mock.patch("piptools.sync.check_call")
def test_sync_ask_declined(check_call, runner):
"""
Make sure nothing is installed if the confirmation is declined
"""
with open("requirements.txt", "w") as req_in:
req_in.write("small-fake-a==1.10.0")

runner.invoke(cli, ["--ask"], input="n\n")

check_call.assert_not_called()


@mock.patch("piptools.sync.check_call")
def test_sync_ask_accepted(check_call, runner):
"""
Make sure pip is called when the confirmation is accepted (even if
--dry-run is given)
"""
with open("requirements.txt", "w") as req_in:
req_in.write("small-fake-a==1.10.0")

runner.invoke(cli, ["--ask", "--dry-run"], input="y\n")

assert check_call.call_count == 2
62 changes: 52 additions & 10 deletions tests/test_sync.py
Original file line number Diff line number Diff line change
Expand Up @@ -403,39 +403,81 @@ def test_sync_verbose(check_call, from_line):
assert "-q" not in check_call_args


@pytest.mark.parametrize(
("to_install", "to_uninstall", "expected_message"),
[
({"django==1.8", "click==4.0"}, set(), "Would install:"),
(set(), {"django==1.8", "click==4.0"}, "Would uninstall:"),
],
)
@mock.patch("piptools.sync.click.echo")
def test_sync_dry_run_would_install(echo, from_line):
def test_sync_dry_run(echo, from_line, to_install, to_uninstall, expected_message):
"""
Sync with --dry-run option prints what's is going to be installed.
Sync with --dry-run option prints what's is going to be installed/uninstalled.
"""
to_install = {from_line("django==1.8"), from_line("click==4.0")}
to_install = set(from_line(pkg) for pkg in to_install)
to_uninstall = set(from_line(pkg) for pkg in to_uninstall)

sync(to_install, set(), dry_run=True)
sync(to_install, to_uninstall, dry_run=True)

expected_calls = [
mock.call("Would install:"),
mock.call(expected_message),
mock.call(" django==1.8"),
mock.call(" click==4.0"),
]
echo.assert_has_calls(expected_calls, any_order=True)


@pytest.mark.parametrize(
("to_install", "to_uninstall", "expected_message"),
[
({"django==1.8", "click==4.0"}, set(), "Would install:"),
(set(), {"django==1.8", "click==4.0"}, "Would uninstall:"),
],
)
@mock.patch("piptools.sync.check_call")
@mock.patch("piptools.sync.click.confirm")
@mock.patch("piptools.sync.click.echo")
def test_sync_dry_run_would_uninstall(echo, from_line):
def test_sync_ask_declined(
echo, confirm, check_call, from_line, to_install, to_uninstall, expected_message
):
"""
Sync with --dry-run option prints what is going to be uninstalled.
Sync with --ask option does a dry run if the user declines
"""
to_uninstall = {from_line("django==1.8"), from_line("click==4.0")}
confirm.return_value = False

to_install = set(from_line(pkg) for pkg in to_install)
to_uninstall = set(from_line(pkg) for pkg in to_uninstall)

sync(set(), to_uninstall, dry_run=True)
sync(to_install, to_uninstall, ask=True)

expected_calls = [
mock.call("Would uninstall:"),
mock.call(expected_message),
mock.call(" django==1.8"),
mock.call(" click==4.0"),
]
echo.assert_has_calls(expected_calls, any_order=True)

confirm.assert_called_once_with("Would you like to proceed with these changes?")
check_call.assert_not_called()


@pytest.mark.parametrize("dry_run", [True, False])
@mock.patch("piptools.sync.click.confirm")
@mock.patch("piptools.sync.check_call")
def test_sync_ask_accepted(check_call, confirm, from_line, dry_run):
"""
pip should be called as normal when the user confirms, even with dry_run
"""
confirm.return_value = True

sync(
{from_line("django==1.8")}, {from_line("click==4.0")}, ask=True, dry_run=dry_run
)
assert check_call.call_count == 2

confirm.assert_called_once_with("Would you like to proceed with these changes?")


@mock.patch("piptools.sync.check_call")
def test_sync_uninstall_pip_command(check_call):
Expand Down

0 comments on commit 34bce45

Please sign in to comment.