Repository: peterbe/hashin

Issue:
Order of hashes shouldn't matter
Consider this example:
```bash
▶ python hashin.py -r ~/songsearch/requirements.txt --dry-run --update-all
--- Old
+++ New
...
 xmltodict==0.11.0 \
-    --hash=sha256:add07d92089ff611badec526912747cf87afd4f9447af6661aca074eeaf32615 \
-    --hash=sha256:8f8d7d40aa28d83f4109a7e8aa86e67a4df202d9538be40c0cb1d70da527b0df
+    --hash=sha256:8f8d7d40aa28d83f4109a7e8aa86e67a4df202d9538be40c0cb1d70da527b0df \
+    --hash=sha256:add07d92089ff611badec526912747cf87afd4f9447af6661aca074eeaf32615
```

Nothing has actually changed but for some reason, the order of the hashes is different this time. That would cause an unnecessary change. 

Discussion:

Patch:
diff --git a/README.rst b/README.rst
index 7cddf5e..45e29eb 100644
--- a/README.rst
+++ b/README.rst
@@ -254,6 +254,10 @@ Version History
 
 next
 
+  * Order of hashes should not affect if a package in the requirements file
+    should be replaced or not.
+    See https://github.com/peterbe/hashin/issues/93
+
   * (Internal) All tests have been rewritten as plain pytest functions.
 
   * In Python 3, if the package can't be found you get a more explicit exception
diff --git a/hashin.py b/hashin.py
index 3ed9677..bbec469 100755
--- a/hashin.py
+++ b/hashin.py
@@ -230,10 +230,26 @@ def run_packages(
 
 
 def amend_requirements_content(requirements, all_new_lines):
-
     # I wish we had types!
     assert isinstance(all_new_lines, list), type(all_new_lines)
 
+    padding = " " * 4
+
+    def is_different_lines(package, new_lines):
+        # This assumes that for sure the package is already mentioned in the old
+        # requirements. Now we just need to double-check that they really are
+        # different.
+        # The 'new_lines` is what we might intend to replace it with.
+        lines = set()
+        for line in requirements.splitlines():
+            if regex.search(line):
+                lines.add(line.strip(" \\"))
+            elif lines and line.startswith(padding):
+                lines.add(line.strip(" \\"))
+            elif lines:
+                break
+        return lines != set([x.strip(" \\") for x in new_lines.splitlines()])
+
     for package, new_lines in all_new_lines:
         regex = re.compile(
             r"(^|\n|\n\r){0}==|(^|\n|\n\r){0}\[.*\]==".format(re.escape(package)),
@@ -245,10 +261,9 @@ def amend_requirements_content(requirements, all_new_lines):
             if requirements:
                 requirements = requirements.strip() + "\n"
             requirements += new_lines.strip() + "\n"
-        else:
+        elif is_different_lines(package, new_lines):
             # need to replace the existing
             lines = []
-            padding = " " * 4
             for line in requirements.splitlines():
                 if regex.search(line):
                     lines.append(line)
@@ -509,11 +524,8 @@ def get_package_hashes(
         else:
             raise PackageError("No releases could be found for {0}".format(version))
 
-    # Sorting them helps make sure the results are more predictable
-    # when running more than once for the same version
-    hashes = sorted(
-        get_releases_hashes(releases=releases, algorithm=algorithm, verbose=verbose),
-        key=lambda x: x["hash"],
+    hashes = list(
+        get_releases_hashes(releases=releases, algorithm=algorithm, verbose=verbose)
     )
     return {"package": package, "version": version, "hashes": hashes}
 


Test patch:
diff --git a/tests/test_cli.py b/tests/test_cli.py
index f48b3de..3615e37 100644
--- a/tests/test_cli.py
+++ b/tests/test_cli.py
@@ -304,6 +304,55 @@ autocompeter==1.2.3
     assert result == new_lines[1]
 
 
+def test_amend_requirements_content_actually_not_replacement():
+    requirements = (
+        """
+autocompeter==1.2.2
+    --hash=sha256:33a5d0145e82326e781ddee1ad375f92cb84f8cfafea56e9504682adff64a5ee
+    --hash=sha256:4d64ed1b9e0e73095f5cfa87f0e97ddb4c840049e8efeb7e63b46118ba1d623a
+    """.strip()
+        + "\n"
+    )
+
+    new_lines = (
+        "autocompeter",
+        """
+autocompeter==1.2.2
+    --hash=sha256:4d64ed1b9e0e73095f5cfa87f0e97ddb4c840049e8efeb7e63b46118ba1d623a
+    --hash=sha256:33a5d0145e82326e781ddee1ad375f92cb84f8cfafea56e9504682adff64a5ee
+    """.strip()
+        + "\n",
+    )
+
+    result = hashin.amend_requirements_content(requirements, [new_lines])
+    # It should be unchanged because the only thing that changed was the
+    # order of the --hash lines.
+    assert result == requirements
+
+
+def test_amend_requirements_content_replacement_addition():
+    requirements = (
+        """
+autocompeter==1.2.2
+    --hash=sha256:33a5d0145e82326e781ddee1ad375f92cb84f8cfafea56e9504682adff64a5ee
+    """.strip()
+        + "\n"
+    )
+
+    new_lines = (
+        "autocompeter",
+        """
+autocompeter==1.2.2
+    --hash=sha256:4d64ed1b9e0e73095f5cfa87f0e97ddb4c840049e8efeb7e63b46118ba1d623a
+    --hash=sha256:33a5d0145e82326e781ddee1ad375f92cb84f8cfafea56e9504682adff64a5ee
+    """.strip()
+        + "\n",
+    )
+
+    result = hashin.amend_requirements_content(requirements, [new_lines])
+    assert result == new_lines[1]
+
+
 def test_amend_requirements_content_replacement_single_to_multi():
     """Change from autocompeter==1.2.2 to autocompeter==1.2.3
     when it was previously written as a single line and now
@@ -512,21 +561,21 @@ def test_run(murlopen, tmpfile, capsys):
         assert output.endswith("\n")
         lines = output.splitlines()
         assert lines[0] == "hashin==0.10 \\"
-        assert lines[1] == (
+        assert (
             "    --hash=sha512:0d63bf4c115154781846ecf573049324f06b021a1"
             "d4b92da4fae2bf491da2b83a13096b14d73e73cefad36855f4fa936bac4"
             "b2357dabf05a2b1e7329ff1e5455 \\"
-        )
-        assert lines[2] == (
+        ) in lines
+        assert (
             "    --hash=sha512:45d1c5d2237a3b4f78b4198709fb2ecf1f781c823"
             "4ce3d94356f2100a36739433952c6c13b2843952f608949e6baa9f95055"
             "a314487cd8fb3f9d76522d8edb50 \\"
-        )
-        assert lines[3] == (
+        ) in lines
+        assert (
             "    --hash=sha512:c32e6d9fb09dc36ab9222c4606a1f43a2dcc183a8"
             "c64bdd9199421ef779072c174fa044b155babb12860cf000e36bc4d3586"
             "94fa22420c997b1dd75b623d4daa"
-        )
+        ) in lines
 
 
 def test_run_atomic_not_write_with_error_on_last_package(murlopen, tmpfile):
@@ -1449,10 +1498,10 @@ def test_get_package_hashes_unknown_algorithm(murlopen, capsys):
         "version": "0.10",
         "hashes": [
             {
-                "hash": "0d63bf4c115154781846ecf573049324f06b021a1d4b92da4fae2bf491da2b83a13096b14d73e73cefad36855f4fa936bac4b2357dabf05a2b1e7329ff1e5455"
+                "hash": "45d1c5d2237a3b4f78b4198709fb2ecf1f781c8234ce3d94356f2100a36739433952c6c13b2843952f608949e6baa9f95055a314487cd8fb3f9d76522d8edb50"
             },
             {
-                "hash": "45d1c5d2237a3b4f78b4198709fb2ecf1f781c8234ce3d94356f2100a36739433952c6c13b2843952f608949e6baa9f95055a314487cd8fb3f9d76522d8edb50"
+                "hash": "0d63bf4c115154781846ecf573049324f06b021a1d4b92da4fae2bf491da2b83a13096b14d73e73cefad36855f4fa936bac4b2357dabf05a2b1e7329ff1e5455"
             },
             {
                 "hash": "c32e6d9fb09dc36ab9222c4606a1f43a2dcc183a8c64bdd9199421ef779072c174fa044b155babb12860cf000e36bc4d358694fa22420c997b1dd75b623d4daa"