1import collections 2import enum 3import hashlib 4import os 5import re 6import string 7from typing import Literal, Final 8 9 10def write_file(filename: str, new_contents: str) -> None: 11 """Write new content to file, iff the content changed.""" 12 try: 13 with open(filename, encoding="utf-8") as fp: 14 old_contents = fp.read() 15 16 if old_contents == new_contents: 17 # no change: avoid modifying the file modification time 18 return 19 except FileNotFoundError: 20 pass 21 # Atomic write using a temporary file and os.replace() 22 filename_new = f"{filename}.new" 23 with open(filename_new, "w", encoding="utf-8") as fp: 24 fp.write(new_contents) 25 try: 26 os.replace(filename_new, filename) 27 except: 28 os.unlink(filename_new) 29 raise 30 31 32def compute_checksum(input_: str, length: int | None = None) -> str: 33 checksum = hashlib.sha1(input_.encode("utf-8")).hexdigest() 34 if length: 35 checksum = checksum[:length] 36 return checksum 37 38 39def create_regex( 40 before: str, after: str, word: bool = True, whole_line: bool = True 41) -> re.Pattern[str]: 42 """Create a regex object for matching marker lines.""" 43 group_re = r"\w+" if word else ".+" 44 before = re.escape(before) 45 after = re.escape(after) 46 pattern = rf"{before}({group_re}){after}" 47 if whole_line: 48 pattern = rf"^{pattern}$" 49 return re.compile(pattern) 50 51 52class FormatCounterFormatter(string.Formatter): 53 """ 54 This counts how many instances of each formatter 55 "replacement string" appear in the format string. 56 57 e.g. after evaluating "string {a}, {b}, {c}, {a}" 58 the counts dict would now look like 59 {'a': 2, 'b': 1, 'c': 1} 60 """ 61 62 def __init__(self) -> None: 63 self.counts = collections.Counter[str]() 64 65 def get_value( 66 self, key: str, args: object, kwargs: object # type: ignore[override] 67 ) -> Literal[""]: 68 self.counts[key] += 1 69 return "" 70 71 72VersionTuple = tuple[int, int] 73 74 75class Sentinels(enum.Enum): 76 unspecified = "unspecified" 77 unknown = "unknown" 78 79 def __repr__(self) -> str: 80 return f"<{self.value.capitalize()}>" 81 82 83unspecified: Final = Sentinels.unspecified 84unknown: Final = Sentinels.unknown 85 86 87# This one needs to be a distinct class, unlike the other two 88class Null: 89 def __repr__(self) -> str: 90 return '<Null>' 91 92 93NULL = Null() 94