Repository: peterbe/hashin

Issue:
Handle indentation
Would be nice if `hashin` could handle indented files like this one:

https://github.com/liberapay/liberapay.com/blob/master/requirements_base.txt

```
boto3==1.9.85 \
    --hash=sha256:acfd27967cf1ba7f9d83ad6fc2011764541e4c295fe0d896ea7b495cc2f03336 \
    --hash=sha256:96296871863e0245b04931df7dd5c583e53cadbe1d54197829b34b03b0d048a8

    botocore==1.12.85 \
        --hash=sha256:af727d4af0cf1ddbf84eaf1cc9d0160ff066eac7f9e6a2fe6a75ccbed4452c98 \
        --hash=sha256:c381fd05b777f41a608ea0846a8d8ecc32077a83e456d05e824cce8d6b213e32
```

Right now it just inserts a new entry below:
```
$ hashin -r ../../../requirements_base.txt botocore -d
--- Old
+++ New
@@ -258,3 +258,6 @@
     asn1crypto==0.24.0 \
         --hash=sha256:2f1adbb7546ed199e3c90ef23ec95c5cf3585bac7d11fb7eb562a3fe89c64e87 \
         --hash=sha256:9d5c20441baf0cb60a4ac34cc447c6c189024b6b4c6cd7877034f4965c464e49
+botocore==1.12.221 \
+    --hash=sha256:6d49deff062d2ae0f03fc26b56df8b1bb9e8b136657bcd8d84c986a4068fb784 \
+    --hash=sha256:bbee3fdcbe56ca53e2c32c6c12d174fa9b4ffe27b633183c29bd5aec9e200bae
```

Discussion:
peterbe: Note that I edited the PR title and PR description so GitHub would know to automatically close the issue when this PR lands. 

Patch:
diff --git a/hashin.py b/hashin.py
index a053ca1..515e037 100755
--- a/hashin.py
+++ b/hashin.py
@@ -387,43 +387,45 @@ def amend_requirements_content(requirements, 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
+    def is_different_lines(old_lines, new_lines, indent):
+        # This assumes that the package is already mentioned in the old
+        # requirements. Now we just need to double-check that its lines 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, old_name, new_lines in all_new_lines:
+        old = set([l.strip(" \\") for l in old_lines])
+        new = set([indent + x.strip(" \\") for x in new_lines])
+        return old != new
+
+    for package, old_name, new_text in all_new_lines:
         regex = re.compile(
-            r"^{0}(\[.*\])?==".format(re.escape(old_name)), re.IGNORECASE | re.MULTILINE
+            r"^(?P<indent>[ \t]*){0}(\[.*\])?==".format(re.escape(old_name)),
+            re.IGNORECASE | re.MULTILINE,
         )
         # if the package wasn't already there, add it to the bottom
-        if not regex.search(requirements):
+        match = regex.search(requirements)
+        if not match:
             # easy peasy
             if requirements:
                 requirements = requirements.strip() + "\n"
-            requirements += new_lines.strip() + "\n"
-        elif is_different_lines(package, new_lines):
-            # need to replace the existing
+            requirements += new_text.strip() + "\n"
+        else:
+            indent = match.group("indent")
             lines = []
             for line in requirements.splitlines():
                 if regex.search(line):
                     lines.append(line)
-                elif lines and line.startswith(padding):
+                elif lines and line.startswith(indent + padding):
                     lines.append(line)
                 elif lines:
                     break
-            combined = "\n".join(lines + [""])
-            requirements = requirements.replace(combined, new_lines)
+            if is_different_lines(lines, new_text.splitlines(), indent):
+                # need to replace the existing
+                combined = "\n".join(lines + [""])
+                # indent non-empty lines
+                indented = re.sub(
+                    r"^(.+)$", r"{0}\1".format(indent), new_text, flags=re.MULTILINE
+                )
+                requirements = requirements.replace(combined, indented)
 
     return requirements
 


Test patch:
diff --git a/tests/test_cli.py b/tests/test_cli.py
index 8c60ba0..6bf9cc6 100644
--- a/tests/test_cli.py
+++ b/tests/test_cli.py
@@ -579,6 +579,46 @@ selenium==2.53.1 \
     assert new_lines[2] in result
 
 
+def test_amend_requirements_content_indented():
+    """This test came from https://github.com/peterbe/hashin/issues/112"""
+    requirements = (
+        """
+boto3==1.9.85 \\
+    --hash=sha256:acfd27967cf1ba7f9d83ad6fc2011764541e4c295fe0d896ea7b495cc2f03336 \\
+    --hash=sha256:96296871863e0245b04931df7dd5c583e53cadbe1d54197829b34b03b0d048a8
+
+    botocore==1.12.85 \\
+        --hash=sha256:af727d4af0cf1ddbf84eaf1cc9d0160ff066eac7f9e6a2fe6a75ccbed4452c98 \\
+        --hash=sha256:c381fd05b777f41a608ea0846a8d8ecc32077a83e456d05e824cce8d6b213e32
+    """.strip()
+        + "\n"
+    )
+    expect = (
+        """
+boto3==1.9.85 \\
+    --hash=sha256:acfd27967cf1ba7f9d83ad6fc2011764541e4c295fe0d896ea7b495cc2f03336 \\
+    --hash=sha256:96296871863e0245b04931df7dd5c583e53cadbe1d54197829b34b03b0d048a8
+
+    botocore==1.12.221 \\
+        --hash=sha256:6d49deff062d2ae0f03fc26b56df8b1bb9e8b136657bcd8d84c986a4068fb784 \\
+        --hash=sha256:bbee3fdcbe56ca53e2c32c6c12d174fa9b4ffe27b633183c29bd5aec9e200bae
+    """.strip()
+        + "\n"
+    )
+    new_lines = (
+        "botocore",
+        "botocore",
+        """
+botocore==1.12.221 \\
+    --hash=sha256:6d49deff062d2ae0f03fc26b56df8b1bb9e8b136657bcd8d84c986a4068fb784 \\
+    --hash=sha256:bbee3fdcbe56ca53e2c32c6c12d174fa9b4ffe27b633183c29bd5aec9e200bae
+    """.strip()
+        + "\n",
+    )
+    result = hashin.amend_requirements_content(requirements, [new_lines])
+    assert result == expect
+
+
 def test_run(murlopen, tmpfile, capsys):
     def mocked_get(url, **options):
         if url == "https://pypi.org/pypi/hashin/json":