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