• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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