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

mgedmin / check-python-versions / 20389687454

21 Nov 2025 06:38AM UTC coverage: 100.0%. Remained the same
20389687454

push

github

mgedmin
Add experimental tox environments for various type checkers

They're all full of sadness and false positives.

341 of 341 branches covered (100.0%)

Branch coverage included in aggregate %.

1494 of 1494 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 · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc