Repository: jazzband/pip-tools

Issue:
pip-compile --upgrade and --upgrade-package needn't be mutually exclusive
Related to #829. I test some Django packages, with one requirements file for each. Sometimes I want to upgrade all the requirements in my files *except* Django, which I pin for each by passing e.g. `--upgrade-package 'Django>=2.1,<2.2`.

Currently the options are mutually exclusive but I think it makes sense to allow `pip-compile --upgrade --upgrade-package 'Django>=2.1,<2.2` to mean "Upgrade all packages, except from add this constraint on Django.

##### Environment Versions

1. macOS
1. Python version: Pythons 3.5-3.7
1. pip version: 19.1.1
1. pip-tools version: 3.7.0

##### Steps to replicate

See description

##### Expected result

The pins combine

##### Actual result

Error "Only one of --upgrade or --upgrade-package can be provided as an argument"

pip-compile --upgrade and --upgrade-package needn't be mutually exclusive
Related to #829. I test some Django packages, with one requirements file for each. Sometimes I want to upgrade all the requirements in my files *except* Django, which I pin for each by passing e.g. `--upgrade-package 'Django>=2.1,<2.2`.

Currently the options are mutually exclusive but I think it makes sense to allow `pip-compile --upgrade --upgrade-package 'Django>=2.1,<2.2` to mean "Upgrade all packages, except from add this constraint on Django.

##### Environment Versions

1. macOS
1. Python version: Pythons 3.5-3.7
1. pip version: 19.1.1
1. pip-tools version: 3.7.0

##### Steps to replicate

See description

##### Expected result

The pins combine

##### Actual result

Error "Only one of --upgrade or --upgrade-package can be provided as an argument"



Discussion:

Resolving patch:
diff --git a/piptools/scripts/compile.py b/piptools/scripts/compile.py
--- a/piptools/scripts/compile.py
+++ b/piptools/scripts/compile.py
@@ -225,11 +225,6 @@ def cli(
         # Close the file at the end of the context execution
         ctx.call_on_close(safecall(output_file.close_intelligently))
 
-    if upgrade and upgrade_packages:
-        raise click.BadParameter(
-            "Only one of --upgrade or --upgrade-package can be provided as an argument."
-        )
-
     ###
     # Setup
     ###
@@ -260,7 +255,12 @@ def cli(
     session = pip_command._build_session(pip_options)
     repository = PyPIRepository(pip_options, session, build_isolation)
 
-    upgrade_install_reqs = {}
+    # Parse all constraints coming from --upgrade-package/-P
+    upgrade_reqs_gen = (install_req_from_line(pkg) for pkg in upgrade_packages)
+    upgrade_install_reqs = {
+        key_from_req(install_req.req): install_req for install_req in upgrade_reqs_gen
+    }
+
     # Proxy with a LocalRequirementsRepository if --upgrade is not specified
     # (= default invocation)
     if not upgrade and os.path.exists(output_file.name):
@@ -270,14 +270,9 @@ def cli(
             session=repository.session,
             options=pip_options,
         )
-        # Exclude packages from --upgrade-package/-P from
-        # the existing pins: We want to upgrade.
-        upgrade_reqs_gen = (install_req_from_line(pkg) for pkg in upgrade_packages)
-        upgrade_install_reqs = {
-            key_from_req(install_req.req): install_req
-            for install_req in upgrade_reqs_gen
-        }
 
+        # Exclude packages from --upgrade-package/-P from the existing
+        # constraints
         existing_pins = {
             key_from_req(ireq.req): ireq
             for ireq in ireqs


Test patch:
diff --git a/tests/test_cli_compile.py b/tests/test_cli_compile.py
--- a/tests/test_cli_compile.py
+++ b/tests/test_cli_compile.py
@@ -378,6 +378,21 @@ def test_upgrade_packages_option(runner):
     assert "small-fake-b==0.3" in out.output
 
 
+def test_upgrade_packages_option_no_existing_file(runner):
+    """
+    piptools respects --upgrade-package/-P inline list when the output file
+    doesn't exist.
+    """
+    with open("requirements.in", "w") as req_in:
+        req_in.write("small-fake-a\nsmall-fake-b")
+
+    out = runner.invoke(cli, ["-P", "small-fake-b", "-f", MINIMAL_WHEELS_PATH])
+
+    assert out.exit_code == 0
+    assert "small-fake-a==0.2" in out.output
+    assert "small-fake-b==0.3" in out.output
+
+
 def test_upgrade_packages_version_option(runner):
     """
     piptools respects --upgrade-package/-P inline list with specified versions.
@@ -394,6 +409,56 @@ def test_upgrade_packages_version_option(runner):
     assert "small-fake-b==0.2" in out.output
 
 
+def test_upgrade_packages_version_option_no_existing_file(runner):
+    """
+    piptools respects --upgrade-package/-P inline list with specified versions.
+    """
+    with open("requirements.in", "w") as req_in:
+        req_in.write("small-fake-a\nsmall-fake-b")
+
+    out = runner.invoke(cli, ["-P", "small-fake-b==0.2", "-f", MINIMAL_WHEELS_PATH])
+
+    assert out.exit_code == 0
+    assert "small-fake-a==0.2" in out.output
+    assert "small-fake-b==0.2" in out.output
+
+
+def test_upgrade_packages_version_option_and_upgrade(runner):
+    """
+    piptools respects --upgrade-package/-P inline list with specified versions
+    whilst also doing --upgrade.
+    """
+    with open("requirements.in", "w") as req_in:
+        req_in.write("small-fake-a\nsmall-fake-b")
+    with open("requirements.txt", "w") as req_in:
+        req_in.write("small-fake-a==0.1\nsmall-fake-b==0.1")
+
+    out = runner.invoke(
+        cli, ["--upgrade", "-P", "small-fake-b==0.1", "-f", MINIMAL_WHEELS_PATH]
+    )
+
+    assert out.exit_code == 0
+    assert "small-fake-a==0.2" in out.output
+    assert "small-fake-b==0.1" in out.output
+
+
+def test_upgrade_packages_version_option_and_upgrade_no_existing_file(runner):
+    """
+    piptools respects --upgrade-package/-P inline list with specified versions
+    whilst also doing --upgrade and the output file doesn't exist.
+    """
+    with open("requirements.in", "w") as req_in:
+        req_in.write("small-fake-a\nsmall-fake-b")
+
+    out = runner.invoke(
+        cli, ["--upgrade", "-P", "small-fake-b==0.1", "-f", MINIMAL_WHEELS_PATH]
+    )
+
+    assert out.exit_code == 0
+    assert "small-fake-a==0.2" in out.output
+    assert "small-fake-b==0.1" in out.output
+
+
 def test_quiet_option(runner):
     with open("requirements", "w"):
         pass
@@ -554,22 +619,6 @@ def test_multiple_input_files_without_output_file(runner):
     assert out.exit_code == 2
 
 
-def test_mutually_exclusive_upgrade_options(runner):
-    """
-    The options --upgrade and --upgrade-package should be mutual exclusive.
-    """
-    with open("requirements.in", "w") as req_in:
-        req_in.write("six==1.10.0")
-
-    out = runner.invoke(cli, ["--upgrade", "--upgrade-package", "six"])
-
-    assert (
-        "Only one of --upgrade or --upgrade-package can be provided as an argument"
-        in out.output
-    )
-    assert out.exit_code == 2
-
-
 @pytest.mark.parametrize(
     "option, expected",
     [