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

mgedmin / check-python-versions / 25246336596

14 Apr 2026 07:35PM UTC coverage: 100.0%. Remained the same
25246336596

push

github

mgedmin
Back to development: 0.24.3

343 of 343 branches covered (100.0%)

Branch coverage included in aggregate %.

1 of 1 new or added line in 1 file covered. (100.0%)

1 existing line in 1 file now uncovered.

1499 of 1499 relevant lines covered (100.0%)

8.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
9✔
8
import logging
9✔
9
import os
9✔
10
import stat
9✔
11
import subprocess
9✔
12
import sys
9✔
13
from contextlib import contextmanager
9✔
14
from typing import Any, Iterator, Sequence, TextIO, TypeAlias, TypeVar, cast
9✔
15

16

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

19

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

24

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

29

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

34

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

39

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

44

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

52

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

62

63
def pipe(*cmd: str, **kwargs: Any) -> str:
9✔
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:
9✔
71
        log.debug('EXEC cd %s && %s', kwargs['cwd'], ' '.join(cmd))
9✔
72
    else:
73
        log.debug('EXEC %s', ' '.join(cmd))
9✔
74
    p = subprocess.Popen(cmd, stdin=subprocess.DEVNULL, stdout=subprocess.PIPE,
9✔
75
                         **kwargs)
76
    return cast(bytes, p.communicate()[0]).decode('UTF-8', 'replace')
9✔
77

78

79
def confirm_and_update_file(filename: str, new_lines: FileLines) -> None:
9✔
80
    """Update a file with new content, after asking for confirmation."""
81
    if (show_diff(filename, new_lines)
9✔
82
            and confirm(f"Write changes to {filename}?")):
83
        mode = stat.S_IMODE(os.stat(filename).st_mode)
9✔
84
        tempfile = filename + '.tmp'
9✔
85
        with open(tempfile, 'w') as f:
9✔
86
            if hasattr(os, 'fchmod'):
9✔
UNCOV
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)
9✔
92
        try:
9✔
93
            os.rename(tempfile, filename)
9✔
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(
9✔
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:
9✔
106
        old_lines = f.readlines()
9✔
107
    print_diff(old_lines, new_lines, f.name)
9✔
108
    return old_lines != new_lines
9✔
109

110

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

119

120
def confirm(prompt: str) -> bool:
9✔
121
    """Ask the user to confirm an action."""
122
    while True:
9✔
123
        try:
9✔
124
            answer = input(f'{prompt} [y/N] ').strip().lower()
9✔
125
        except EOFError:
9✔
126
            answer = ""
9✔
127
        if answer == 'y':
9✔
128
            print()
9✔
129
            return True
9✔
130
        if answer == 'n' or not answer:
9✔
131
            print()
9✔
132
            return False
9✔
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