Repository: googleapis/synthtool

Issue:
Add ability to mark a replacement as required
For some libraries (like Grafeas), we are doing source replacement that must be done. It's possible that the source code changes (generator changes, formatting, etc) and the replacement fails to match. We already warn if the replacement doesn't happen which is great when it's for a bug fix. When it's a required replacement, we should give the option to make synth fail.

Perhaps, something like:
```python
s.replace(source_path, search, replace, required=True)
```

Comments:

Patch:
diff --git a/synthtool/transforms.py b/synthtool/transforms.py
index 2d10984..1fb43ec 100644
--- a/synthtool/transforms.py
+++ b/synthtool/transforms.py
@@ -185,35 +185,37 @@ def _replace_in_file(path, expr, replacement):
 
         # Don't bother writing the file if we didn't change
         # anything.
-        if not count:
-            return False
-
-        fh.seek(0)
-        fh.write(content)
-        fh.truncate()
-
-    return True
+        if count:
+            fh.seek(0)
+            fh.write(content)
+            fh.truncate()
+        return count
 
 
 def replace(
     sources: ListOfPathsOrStrs, before: str, after: str, flags: int = re.MULTILINE
-):
-    """Replaces occurrences of before with after in all the given sources."""
+) -> int:
+    """Replaces occurrences of before with after in all the given sources.
+
+    Returns:
+      The number of times the text was found and replaced across all files.
+    """
     expr = re.compile(before, flags=flags or 0)
     paths = _filter_files(_expand_paths(sources, "."))
 
     if not paths:
         log.warning(f"No files were found in sources {sources} for replace()")
 
-    any_replaced = False
+    count_replaced = 0
     for path in paths:
         replaced = _replace_in_file(path, expr, after)
-        any_replaced = any_replaced or replaced
+        count_replaced += replaced
         if replaced:
             log.info(f"Replaced {before!r} in {path}.")
 
-    if not any_replaced:
+    if not count_replaced:
         log.warning(
             f"No replacements made in {sources} for pattern {before}, maybe "
             "replacement is not longer needed?"
         )
+    return count_replaced


Test patch:
diff --git a/tests/test_transforms.py b/tests/test_transforms.py
index 0f37382..d26812a 100644
--- a/tests/test_transforms.py
+++ b/tests/test_transforms.py
@@ -27,18 +27,18 @@ from synthtool import _tracked_paths
 
 @pytest.fixture()
 def expand_path_fixtures(tmpdir):
-    files = [
-        "a.txt",
-        "b.py",
-        "c.md",
-        normpath("dira/e.txt"),
-        normpath("dira/f.py"),
-        normpath("dirb/suba/g.py"),
-    ]
-
-    for file in files:
-        path = tmpdir.join(file)
-        path.write_text("content", encoding="utf-8", ensure=True)
+    files = {
+        "a.txt": "alpha text",
+        "b.py": "beta python",
+        "c.md": "see markdown",
+        "dira/e.txt": "epsilon text",
+        "dira/f.py": "eff python",
+        "dirb/suba/g.py": "gamma python",
+    }
+
+    for file_name, content in files.items():
+        path = tmpdir.join(normpath(file_name))
+        path.write_text(content, encoding="utf-8", ensure=True)
 
     cwd = os.getcwd()
     os.chdir(str(tmpdir))
@@ -165,3 +165,24 @@ def test__move_to_dest_subdir(expand_path_fixtures):
 
     # Assert destination does not contain dira/f.py (excluded)
     assert files == [normpath("dest/dira"), normpath("dest/dira/e.txt")]
+
+
+def test_simple_replace(expand_path_fixtures):
+    count_replaced = transforms.replace(["a.txt", "b.py"], "b..a", "GA")
+    assert 1 == count_replaced
+    assert "alpha text" == open("a.txt", "rt").read()
+    assert "GA python" == open("b.py", "rt").read()
+
+
+def test_multi_replace(expand_path_fixtures):
+    count_replaced = transforms.replace(["a.txt", "b.py"], r"(\w+)a", r"\1z")
+    assert 2 == count_replaced
+    assert "alphz text" == open("a.txt", "rt").read()
+    assert "betz python" == open("b.py", "rt").read()
+
+
+def test_replace_not_found(expand_path_fixtures):
+    count_replaced = transforms.replace(["a.txt", "b.py"], r"z", r"q")
+    assert 0 == count_replaced
+    assert "alpha text" == open("a.txt", "rt").read()
+    assert "beta python" == open("b.py", "rt").read()