Repository: jazzband/pip-tools

Issue:
pip-compile --upgrade-package removes "via" annotation from lock file
`pip-compile --upgrade` removes source annotations (e.g. `# via jinja2`).

#### Environment Versions

1. OS Type: Linux
1. Python version: `3.6.6`
1. pip version:  `19.2.3`
1. pip-tools version: `4.1.0`

#### Steps to replicate

Prepare virtualenv:

```shell
$ pyenv install 3.6.6 py36
$ pyenv activate py36
$ pip install -U pip pip-tools
```

Prepare lock file:

```shell
$ echo "jinja2" > requirements.in
$ pip-compile --no-header --no-index
jinja2==2.10.3
markupsafe==1.1.1         # via jinja2
$ # edit requirements.txt and change markupsafe version to 1.0.0
$ cat requirements.txt
jinja2==2.10.3
markupsafe==1.0.0         # via jinja2
```

Let's upgrade markupsafe:

```shell
$ pip-compile -P markupsafe --no-header --no-index
jinja2==2.10.3
markupsafe==1.1.1
```

Problem is that `# via jinja2` marker was removed. `--upgrade-package` shouldn't remove the annotation, as intention was merely to upgrade it. It is still required via jinja2 and not directly (it's not in `requirements.in`)

On the other hand, I understand that I now directly influenced the package version, so annotation gets removed. But still, my initial assumption was that marker would remain.

So I am not sure about this one. Is current behavior intentional?


Discussion:
Hello @tuukkamustonen,

Thanks for the good catch and easy reproducer!

> So I am not sure about this one. Is current behavior intentional?

It's a bug.
That's because packages to update accidentally become primary packages. For those who want to fix it:

```diff
diff --git a/piptools/scripts/compile.py b/piptools/scripts/compile.py
index 6415227..9624684 100755
--- a/piptools/scripts/compile.py
+++ b/piptools/scripts/compile.py
@@ -326,6 +326,10 @@ def cli(
                 )
             )
 
+    primary_packages = {
+        key_from_ireq(ireq) for ireq in constraints if not ireq.constraint
+    }
+
     constraints.extend(upgrade_install_reqs.values())
 
     # Filter out pip environment markers which do not match (PEP496)
@@ -415,9 +419,7 @@ def cli(
         results=results,
         unsafe_requirements=resolver.unsafe_constraints,
         reverse_dependencies=reverse_dependencies,
-        primary_packages={
-            key_from_ireq(ireq) for ireq in constraints if not ireq.constraint
-        },
+        primary_packages=primary_packages,
         markers={
             key_from_ireq(ireq): ireq.markers for ireq in constraints if ireq.markers
         },

```

Resolving patch:
diff --git a/piptools/scripts/compile.py b/piptools/scripts/compile.py
--- a/piptools/scripts/compile.py
+++ b/piptools/scripts/compile.py
@@ -326,6 +326,10 @@ def cli(
                 )
             )
 
+    primary_packages = {
+        key_from_ireq(ireq) for ireq in constraints if not ireq.constraint
+    }
+
     constraints.extend(upgrade_install_reqs.values())
 
     # Filter out pip environment markers which do not match (PEP496)
@@ -415,9 +419,7 @@ def cli(
         results=results,
         unsafe_requirements=resolver.unsafe_constraints,
         reverse_dependencies=reverse_dependencies,
-        primary_packages={
-            key_from_ireq(ireq) for ireq in constraints if not ireq.constraint
-        },
+        primary_packages=primary_packages,
         markers={
             key_from_ireq(ireq): ireq.markers for ireq in constraints if ireq.markers
         },


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
@@ -647,8 +647,8 @@ def test_multiple_input_files_without_output_file(runner):
 @pytest.mark.parametrize(
     "option, expected",
     [
-        ("--annotate", "small-fake-a==0.1         # via small-fake-with-deps-a\n"),
-        ("--no-annotate", "small-fake-a==0.1\n"),
+        ("--annotate", "small-fake-a==0.2         # via small-fake-with-deps-a\n"),
+        ("--no-annotate", "small-fake-a==0.2\n"),
     ],
 )
 def test_annotate_option(pip_conf, runner, option, expected):
@@ -839,3 +839,29 @@ def test_empty_input_file_no_header(runner, empty_input_pkg, prior_output_pkg):
 
     with open("requirements.txt", "r") as req_txt:
         assert req_txt.read().strip() == ""
+
+
+def test_upgrade_package_doesnt_remove_annotation(pip_conf, runner):
+    """
+    Tests pip-compile --upgrade-package shouldn't remove "via" annotation.
+    See: GH-929
+    """
+    with open("requirements.in", "w") as req_in:
+        req_in.write("small-fake-with-deps-a\n")
+
+    runner.invoke(cli)
+
+    # Downgrade small-fake-a to 0.1
+    with open("requirements.txt", "w") as req_txt:
+        req_txt.write(
+            "small-fake-with-deps-a==0.1\n"
+            "small-fake-a==0.1         # via small-fake-with-deps-a\n"
+        )
+
+    runner.invoke(cli, ["-P", "small-fake-a"])
+
+    with open("requirements.txt", "r") as req_txt:
+        assert (
+            "small-fake-a==0.2         # via small-fake-with-deps-a"
+            in req_txt.read().splitlines()
+        )
diff --git a/tests/test_data/minimal_wheels/small_fake_with_deps_a-0.1-py2.py3-none-any.whl b/tests/test_data/minimal_wheels/small_fake_with_deps_a-0.1-py2.py3-none-any.whl
Binary files a/tests/test_data/minimal_wheels/small_fake_with_deps_a-0.1-py2.py3-none-any.whl and b/tests/test_data/minimal_wheels/small_fake_with_deps_a-0.1-py2.py3-none-any.whl differ
diff --git a/tests/test_data/packages/small_fake_with_deps_a/setup.py b/tests/test_data/packages/small_fake_with_deps_a/setup.py
--- a/tests/test_data/packages/small_fake_with_deps_a/setup.py
+++ b/tests/test_data/packages/small_fake_with_deps_a/setup.py
@@ -1,5 +1,5 @@
 from setuptools import setup
 
 setup(
-    name="small_fake_with_deps_a", version=0.1, install_requires=["small-fake-a==0.1"]
+    name="small_fake_with_deps_a", version=0.1, install_requires=["small-fake-a==0.2"]
 )