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

mgedmin / check-python-versions / 36233497228

24 Jul 2026 04:02PM UTC coverage: 100.0%. Remained the same
36233497228

push

github

mgedmin
Mark pypy3.11 on windows-latest as optional

686 of 686 branches covered (100.0%)

Branch coverage included in aggregate %.

1499 of 1499 relevant lines covered (100.0%)

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

16

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

19

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

24

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

29

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

34

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

39

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

44

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

52

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

62

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

78

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

110

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

119

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

© 2026 Coveralls, Inc