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

mgedmin / check-python-versions / 18611240295

14 Oct 2025 06:18AM UTC coverage: 100.0%. Remained the same
18611240295

push

github

mgedmin
Back to development: 0.23.1

681 of 681 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.

1496 of 1496 relevant lines covered (100.0%)

15.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
16✔
8
import logging
16✔
9
import os
16✔
10
import stat
16✔
11
import subprocess
16✔
12
import sys
16✔
13
from contextlib import contextmanager
16✔
14
from typing import (
16✔
15
    Any,
16
    Iterator,
17
    List,
18
    Sequence,
19
    TextIO,
20
    Tuple,
21
    TypeVar,
22
    Union,
23
    cast,
24
)
25

26

27
log = logging.getLogger('check-python-versions')
16✔
28

29

30
T = TypeVar('T')
16✔
31
OneOrMore = Union[T, Sequence[T]]
16✔
32
OneOrTuple = Union[T, Tuple[T, ...]]
16✔
33

34

35
FileObjectWithName = TextIO  # also has a .name attribute
16✔
36
FileOrFilename = Union[str, FileObjectWithName]
16✔
37
FileLines = List[str]
16✔
38

39

40
def get_indent(line: str) -> str:
16✔
41
    """Return the indentation part of a line of text."""
42
    return line[:-len(line.lstrip())]
16✔
43

44

45
def warn(msg: str) -> None:
16✔
46
    """Print a warning to standard error."""
47
    print(msg, file=sys.stderr)
16✔
48

49

50
def is_file_object(filename_or_file_object: FileOrFilename) -> bool:
16✔
51
    """Is this a file-like object?"""
52
    return hasattr(filename_or_file_object, 'read')
16✔
53

54

55
def file_name(filename_or_file_object: FileOrFilename) -> str:
16✔
56
    """Return the name of the file."""
57
    if is_file_object(filename_or_file_object):
16✔
58
        return cast(TextIO, filename_or_file_object).name
16✔
59
    else:
60
        return str(filename_or_file_object)
16✔
61

62

63
@contextmanager
16✔
64
def open_file(filename_or_file_object: FileOrFilename) -> Iterator[TextIO]:
16✔
65
    """Context manager for opening files."""
66
    if is_file_object(filename_or_file_object):
16✔
67
        yield cast(TextIO, filename_or_file_object)
16✔
68
    else:
69
        with open(cast(str, filename_or_file_object)) as fp:
16✔
70
            yield fp
16✔
71

72

73
def pipe(*cmd: str, **kwargs: Any) -> str:
16✔
74
    """Run a subprocess and return its standard output.
75

76
    Keyword arguments are passed directly to `subprocess.Popen`.
77

78
    Standard input and standard error are not redirected.
79
    """
80
    if 'cwd' in kwargs:
16✔
81
        log.debug('EXEC cd %s && %s', kwargs['cwd'], ' '.join(cmd))
16✔
82
    else:
83
        log.debug('EXEC %s', ' '.join(cmd))
16✔
84
    p = subprocess.Popen(cmd, stdin=subprocess.DEVNULL, stdout=subprocess.PIPE,
16✔
85
                         **kwargs)
86
    return cast(bytes, p.communicate()[0]).decode('UTF-8', 'replace')
16✔
87

88

89
def confirm_and_update_file(filename: str, new_lines: FileLines) -> None:
16✔
90
    """Update a file with new content, after asking for confirmation."""
91
    if (show_diff(filename, new_lines)
16✔
92
            and confirm(f"Write changes to {filename}?")):
93
        mode = stat.S_IMODE(os.stat(filename).st_mode)
16✔
94
        tempfile = filename + '.tmp'
16✔
95
        with open(tempfile, 'w') as f:
16✔
96
            if hasattr(os, 'fchmod'):
16✔
UNCOV
97
                os.fchmod(f.fileno(), mode)
10✔
98
            else:  # pragma: windows
99
                # Windows, what else?
100
                os.chmod(tempfile, mode)
101
            f.writelines(new_lines)
16✔
102
        try:
16✔
103
            os.rename(tempfile, filename)
16✔
104
        except FileExistsError:  # pragma: windows
105
            # No atomic replace on Windows
106
            os.unlink(filename)
107
            os.rename(tempfile, filename)
108

109

110
def show_diff(
16✔
111
    filename_or_file_object: FileOrFilename,
112
    new_lines: FileLines
113
) -> bool:
114
    """Show the difference between two versions of a file."""
115
    with open_file(filename_or_file_object) as f:
16✔
116
        old_lines = f.readlines()
16✔
117
    print_diff(old_lines, new_lines, f.name)
16✔
118
    return old_lines != new_lines
16✔
119

120

121
def print_diff(a: List[str], b: List[str], filename: str) -> None:
16✔
122
    """Show the difference between two versions of a file."""
123
    print(''.join(difflib.unified_diff(
16✔
124
        a, b,
125
        filename, filename,
126
        "(original)", "(updated)",
127
    )))
128

129

130
def confirm(prompt: str) -> bool:
16✔
131
    """Ask the user to confirm an action."""
132
    while True:
12✔
133
        try:
16✔
134
            answer = input(f'{prompt} [y/N] ').strip().lower()
16✔
135
        except EOFError:
16✔
136
            answer = ""
16✔
137
        if answer == 'y':
16✔
138
            print()
16✔
139
            return True
16✔
140
        if answer == 'n' or not answer:
16✔
141
            print()
16✔
142
            return False
16✔
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

© 2025 Coveralls, Inc