• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

mgedmin / check-python-versions / 27865425364

24 May 2026 05:51PM UTC coverage: 100.0%. Remained the same
27865425364

push

github

mgedmin
Spell actions/checkout correctly

343 of 343 branches covered (100.0%)

Branch coverage included in aggregate %.

1499 of 1499 relevant lines covered (100.0%)

10.99 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

100.0
/src/check_python_versions/utils.py
1
"""
2
Assorted utilities that didn't fit elsewhere.
3

4
Yes, this is a sign of bad design.  Maybe someday I'll clean it up.
5
"""
6

7
import difflib
11✔
8
import logging
11✔
9
import os
11✔
10
import stat
11✔
11
import subprocess
11✔
12
import sys
11✔
13
from contextlib import contextmanager
11✔
14
from typing import Any, Iterator, Sequence, TextIO, TypeAlias, TypeVar, cast
11✔
15

16

17
log = logging.getLogger('check-python-versions')
11✔
18

19

20
T = TypeVar('T')
11✔
21
OneOrMore: TypeAlias = T | Sequence[T]
11✔
22
OneOrTuple: TypeAlias = T | tuple[T, ...]
11✔
23

24

25
FileObjectWithName = TextIO  # also has a .name attribute
11✔
26
FileOrFilename = str | FileObjectWithName
11✔
27
FileLines = list[str]
11✔
28

29

30
def get_indent(line: str) -> str:
11✔
31
    """Return the indentation part of a line of text."""
32
    return line[:-len(line.lstrip())]
11✔
33

34

35
def warn(msg: str) -> None:
11✔
36
    """Print a warning to standard error."""
37
    print(msg, file=sys.stderr)
11✔
38

39

40
def is_file_object(filename_or_file_object: FileOrFilename) -> bool:
11✔
41
    """Is this a file-like object?"""
42
    return hasattr(filename_or_file_object, 'read')
11✔
43

44

45
def file_name(filename_or_file_object: FileOrFilename) -> str:
11✔
46
    """Return the name of the file."""
47
    if is_file_object(filename_or_file_object):
11✔
48
        return cast(TextIO, filename_or_file_object).name
11✔
49
    else:
50
        return str(filename_or_file_object)
11✔
51

52

53
@contextmanager
11✔
54
def open_file(filename_or_file_object: FileOrFilename) -> Iterator[TextIO]:
11✔
55
    """Context manager for opening files."""
56
    if is_file_object(filename_or_file_object):
11✔
57
        yield cast(TextIO, filename_or_file_object)
11✔
58
    else:
59
        with open(cast(str, filename_or_file_object)) as fp:
11✔
60
            yield fp
11✔
61

62

63
def pipe(*cmd: str, **kwargs: Any) -> str:
11✔
64
    """Run a subprocess and return its standard output.
65

66
    Keyword arguments are passed directly to `subprocess.Popen`.
67

68
    Standard input and standard error are not redirected.
69
    """
70
    if 'cwd' in kwargs:
11✔
71
        log.debug('EXEC cd %s && %s', kwargs['cwd'], ' '.join(cmd))
11✔
72
    else:
73
        log.debug('EXEC %s', ' '.join(cmd))
11✔
74
    p = subprocess.Popen(cmd, stdin=subprocess.DEVNULL, stdout=subprocess.PIPE,
11✔
75
                         **kwargs)
76
    return cast(bytes, p.communicate()[0]).decode('UTF-8', 'replace')
11✔
77

78

79
def confirm_and_update_file(filename: str, new_lines: FileLines) -> None:
11✔
80
    """Update a file with new content, after asking for confirmation."""
81
    if (show_diff(filename, new_lines)
11✔
82
            and confirm(f"Write changes to {filename}?")):
83
        mode = stat.S_IMODE(os.stat(filename).st_mode)
11✔
84
        tempfile = filename + '.tmp'
11✔
85
        with open(tempfile, 'w') as f:
11✔
86
            if hasattr(os, 'fchmod'):
11✔
87
                os.fchmod(f.fileno(), mode)
8✔
88
            else:  # pragma: windows
89
                # Windows, what else?
90
                os.chmod(tempfile, mode)
91
            f.writelines(new_lines)
11✔
92
        try:
11✔
93
            os.rename(tempfile, filename)
11✔
94
        except FileExistsError:  # pragma: windows
95
            # No atomic replace on Windows
96
            os.unlink(filename)
97
            os.rename(tempfile, filename)
98

99

100
def show_diff(
11✔
101
    filename_or_file_object: FileOrFilename,
102
    new_lines: FileLines
103
) -> bool:
104
    """Show the difference between two versions of a file."""
105
    with open_file(filename_or_file_object) as f:
11✔
106
        old_lines = f.readlines()
11✔
107
    print_diff(old_lines, new_lines, f.name)
11✔
108
    return old_lines != new_lines
11✔
109

110

111
def print_diff(a: list[str], b: list[str], filename: str) -> None:
11✔
112
    """Show the difference between two versions of a file."""
113
    print(''.join(difflib.unified_diff(
11✔
114
        a, b,
115
        filename, filename,
116
        "(original)", "(updated)",
117
    )))
118

119

120
def confirm(prompt: str) -> bool:
11✔
121
    """Ask the user to confirm an action."""
122
    while True:
11✔
123
        try:
11✔
124
            answer = input(f'{prompt} [y/N] ').strip().lower()
11✔
125
        except EOFError:
11✔
126
            answer = ""
11✔
127
        if answer == 'y':
11✔
128
            print()
11✔
129
            return True
11✔
130
        if answer == 'n' or not answer:
11✔
131
            print()
11✔
132
            return False
11✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc