xref: /aosp_15_r20/external/bazelbuild-rules_python/tools/private/update_deps/update_file.py (revision 60517a1edbc8ecf509223e9af94a7adec7d736b8)
1# Copyright 2023 The Bazel Authors. All rights reserved.
2#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7#    http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
14
15"""A small library to update bazel files within the repo.
16
17This is reused in other files updating coverage deps and pip deps.
18"""
19
20import argparse
21import difflib
22import pathlib
23import sys
24
25
26def _writelines(path: pathlib.Path, out: str):
27    with open(path, "w") as f:
28        f.write(out)
29
30
31def unified_diff(name: str, a: str, b: str) -> str:
32    return "".join(
33        difflib.unified_diff(
34            a.splitlines(keepends=True),
35            b.splitlines(keepends=True),
36            fromfile=f"a/{name}",
37            tofile=f"b/{name}",
38        )
39    ).strip()
40
41
42def replace_snippet(
43    current: str,
44    snippet: str,
45    start_marker: str,
46    end_marker: str,
47) -> str:
48    """Update a file on disk to replace text in a file between two markers.
49
50    Args:
51        path: pathlib.Path, the path to the file to be modified.
52        snippet: str, the snippet of code to insert between the markers.
53        start_marker: str, the text that marks the start of the region to be replaced.
54        end_markr: str, the text that marks the end of the region to be replaced.
55        dry_run: bool, if set to True, then the file will not be written and instead we are going to print a diff to
56            stdout.
57    """
58    lines = []
59    skip = False
60    found_match = False
61    for line in current.splitlines(keepends=True):
62        if line.lstrip().startswith(start_marker.lstrip()):
63            found_match = True
64            lines.append(line)
65            lines.append(snippet.rstrip() + "\n")
66            skip = True
67        elif skip and line.lstrip().startswith(end_marker):
68            skip = False
69            lines.append(line)
70            continue
71        elif not skip:
72            lines.append(line)
73
74    if not found_match:
75        raise RuntimeError(f"Start marker '{start_marker}' was not found")
76    if skip:
77        raise RuntimeError(f"End marker '{end_marker}' was not found")
78
79    return "".join(lines)
80
81
82def update_file(
83    path: pathlib.Path,
84    snippet: str,
85    start_marker: str,
86    end_marker: str,
87    dry_run: bool = True,
88):
89    """update a file on disk to replace text in a file between two markers.
90
91    Args:
92        path: pathlib.Path, the path to the file to be modified.
93        snippet: str, the snippet of code to insert between the markers.
94        start_marker: str, the text that marks the start of the region to be replaced.
95        end_markr: str, the text that marks the end of the region to be replaced.
96        dry_run: bool, if set to True, then the file will not be written and instead we are going to print a diff to
97            stdout.
98    """
99    current = path.read_text()
100    out = replace_snippet(current, snippet, start_marker, end_marker)
101
102    if not dry_run:
103        _writelines(path, out)
104        return
105
106    relative = path.relative_to(
107        pathlib.Path(__file__).resolve().parent.parent.parent.parent
108    )
109    name = f"{relative}"
110    diff = unified_diff(name, current, out)
111    if diff:
112        print(f"Diff of the changes that would be made to '{name}':\n{diff}")
113    else:
114        print(f"'{name}' is up to date")
115