check-python-versions 0.23.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,142 @@
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
8
+ import logging
9
+ import os
10
+ import stat
11
+ import subprocess
12
+ import sys
13
+ from contextlib import contextmanager
14
+ from typing import (
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')
28
+
29
+
30
+ T = TypeVar('T')
31
+ OneOrMore = Union[T, Sequence[T]]
32
+ OneOrTuple = Union[T, Tuple[T, ...]]
33
+
34
+
35
+ FileObjectWithName = TextIO # also has a .name attribute
36
+ FileOrFilename = Union[str, FileObjectWithName]
37
+ FileLines = List[str]
38
+
39
+
40
+ def get_indent(line: str) -> str:
41
+ """Return the indentation part of a line of text."""
42
+ return line[:-len(line.lstrip())]
43
+
44
+
45
+ def warn(msg: str) -> None:
46
+ """Print a warning to standard error."""
47
+ print(msg, file=sys.stderr)
48
+
49
+
50
+ def is_file_object(filename_or_file_object: FileOrFilename) -> bool:
51
+ """Is this a file-like object?"""
52
+ return hasattr(filename_or_file_object, 'read')
53
+
54
+
55
+ def file_name(filename_or_file_object: FileOrFilename) -> str:
56
+ """Return the name of the file."""
57
+ if is_file_object(filename_or_file_object):
58
+ return cast(TextIO, filename_or_file_object).name
59
+ else:
60
+ return str(filename_or_file_object)
61
+
62
+
63
+ @contextmanager
64
+ def open_file(filename_or_file_object: FileOrFilename) -> Iterator[TextIO]:
65
+ """Context manager for opening files."""
66
+ if is_file_object(filename_or_file_object):
67
+ yield cast(TextIO, filename_or_file_object)
68
+ else:
69
+ with open(cast(str, filename_or_file_object)) as fp:
70
+ yield fp
71
+
72
+
73
+ def pipe(*cmd: str, **kwargs: Any) -> str:
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:
81
+ log.debug('EXEC cd %s && %s', kwargs['cwd'], ' '.join(cmd))
82
+ else:
83
+ log.debug('EXEC %s', ' '.join(cmd))
84
+ p = subprocess.Popen(cmd, stdin=subprocess.DEVNULL, stdout=subprocess.PIPE,
85
+ **kwargs)
86
+ return cast(bytes, p.communicate()[0]).decode('UTF-8', 'replace')
87
+
88
+
89
+ def confirm_and_update_file(filename: str, new_lines: FileLines) -> None:
90
+ """Update a file with new content, after asking for confirmation."""
91
+ if (show_diff(filename, new_lines)
92
+ and confirm(f"Write changes to {filename}?")):
93
+ mode = stat.S_IMODE(os.stat(filename).st_mode)
94
+ tempfile = filename + '.tmp'
95
+ with open(tempfile, 'w') as f:
96
+ if hasattr(os, 'fchmod'):
97
+ os.fchmod(f.fileno(), mode)
98
+ else: # pragma: windows
99
+ # Windows, what else?
100
+ os.chmod(tempfile, mode)
101
+ f.writelines(new_lines)
102
+ try:
103
+ os.rename(tempfile, filename)
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(
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:
116
+ old_lines = f.readlines()
117
+ print_diff(old_lines, new_lines, f.name)
118
+ return old_lines != new_lines
119
+
120
+
121
+ def print_diff(a: List[str], b: List[str], filename: str) -> None:
122
+ """Show the difference between two versions of a file."""
123
+ print(''.join(difflib.unified_diff(
124
+ a, b,
125
+ filename, filename,
126
+ "(original)", "(updated)",
127
+ )))
128
+
129
+
130
+ def confirm(prompt: str) -> bool:
131
+ """Ask the user to confirm an action."""
132
+ while True:
133
+ try:
134
+ answer = input(f'{prompt} [y/N] ').strip().lower()
135
+ except EOFError:
136
+ answer = ""
137
+ if answer == 'y':
138
+ print()
139
+ return True
140
+ if answer == 'n' or not answer:
141
+ print()
142
+ return False
@@ -0,0 +1,158 @@
1
+ """Python version business logic."""
2
+
3
+ import re
4
+ from typing import Collection, List, NamedTuple, Optional, Set, Union
5
+
6
+
7
+ #
8
+ # Information about Python releases that needs to be constantly updated as
9
+ # Python makes new releases.
10
+ #
11
+
12
+ MAX_PYTHON_1_VERSION = 6 # i.e. 1.6
13
+ MAX_PYTHON_2_VERSION = 7 # i.e. 2.7
14
+ CURRENT_PYTHON_3_VERSION = 14 # i.e. 3.14
15
+
16
+ MAX_MINOR_FOR_MAJOR = {
17
+ 1: MAX_PYTHON_1_VERSION,
18
+ 2: MAX_PYTHON_2_VERSION,
19
+ 3: CURRENT_PYTHON_3_VERSION,
20
+ }
21
+
22
+
23
+ VERSION_RX = re.compile('^([^-0-9]*)([0-9]*)([.][0-9]+)?(.*)$')
24
+
25
+
26
+ class Version(NamedTuple):
27
+ """A simplified Python version number.
28
+
29
+ Primarily needed so we can sort lists of version numbers correctly, i.e.
30
+ 2.7, 3.0, 3.1, 3.2, ..., 3.9, 3.10, ...
31
+
32
+ Can have an optional prefix, e.g. PyPy3.6 is Version(prefix='PyPy',
33
+ major=3, minor=6).
34
+
35
+ Can have an optional suffix, e.g. 3.10-dev is Version(major=3, minor=10,
36
+ suffix='-dev').
37
+
38
+ Any string can be round-tripped to a Version and back via
39
+ Version.from_string() and Version.__str__.
40
+ """
41
+
42
+ prefix: str = ''
43
+ major: int = -1 # I'd've preferred to use None, but it complicates sorting
44
+ minor: int = -1
45
+ suffix: str = ''
46
+
47
+ @classmethod
48
+ def from_string(cls, v: str) -> 'Version':
49
+ m = VERSION_RX.match(v)
50
+ assert m is not None
51
+ prefix, major, minor, suffix = m.groups()
52
+ return cls(
53
+ prefix,
54
+ int(major) if major else -1,
55
+ int(minor[1:]) if minor else -1,
56
+ suffix,
57
+ )
58
+
59
+ def __repr__(self) -> str:
60
+ return 'Version({})'.format(', '.join(part for part in [
61
+ f'prefix={self.prefix!r}' if self.prefix else '',
62
+ f'major={self.major!r}' if self.major != -1 else '',
63
+ f'minor={self.minor!r}' if self.minor != -1 else '',
64
+ f'suffix={self.suffix!r}' if self.suffix else '',
65
+ ] if part))
66
+
67
+ def __str__(self) -> str:
68
+ major = '' if self.major == -1 else f'{self.major}'
69
+ minor = '' if self.minor == -1 else f'.{self.minor}'
70
+ return f'{self.prefix}{major}{minor}{self.suffix}'
71
+
72
+
73
+ VersionSet = Set[Version]
74
+ VersionList = Collection[Version]
75
+ SortedVersionList = List[Version]
76
+
77
+
78
+ def is_important(v: Union[Version, str]) -> bool:
79
+ """Is the version important for matching purposes?
80
+
81
+ Different sources can express support for different versions, e.g.
82
+ classifiers can express support for "PyPy" but python_requires can't.
83
+ Also some CI systems allow testing on unreleased Python versions that
84
+ cannot be listed in classifiers, so their presence should not cause
85
+ mismatch errors.
86
+ """
87
+ if not isinstance(v, Version):
88
+ v = Version.from_string(v)
89
+ upcoming_release = Version(major=3, minor=CURRENT_PYTHON_3_VERSION + 1)
90
+ return (
91
+ not v.prefix.startswith(('PyPy', 'Jython')) and v.prefix != 'nightly'
92
+ and '-dev' not in v.suffix
93
+ and '-alpha' not in v.suffix
94
+ and '-beta' not in v.suffix
95
+ and '-rc' not in v.suffix
96
+ and v != upcoming_release
97
+ )
98
+
99
+
100
+ def important(versions: Collection[Version]) -> VersionSet:
101
+ """Filter out unimportant versions.
102
+
103
+ See `is_important` for what consitutes "important".
104
+ """
105
+ return {
106
+ v for v in versions
107
+ if is_important(v)
108
+ }
109
+
110
+
111
+ def pypy_versions(versions: Collection[Version]) -> VersionSet:
112
+ """Filter PyPy versions."""
113
+ return {
114
+ v for v in versions
115
+ if v.prefix.startswith('PyPy')
116
+ }
117
+
118
+
119
+ def expand_pypy(versions: Collection[Version]) -> SortedVersionList:
120
+ """Determine whether PyPy support means PyPy2 or PyPy3 or both.
121
+
122
+ Some data sources (like setup.py classifiers) allow you to indicate PyPy
123
+ support without specifying whether you mean PyPy2 or PyPy3. Other data
124
+ sources (like all CI systems) are more explicit. To make these version
125
+ lists directly comparable we need to look at supported CPython versions and
126
+ translate that knowledge into PyPy versions.
127
+ """
128
+ supports_pypy = any(v.prefix == 'PyPy' for v in versions)
129
+ if not supports_pypy:
130
+ return sorted(versions)
131
+ supports_py2 = any(v.major == 2 for v in versions)
132
+ supports_py3 = any(v.major == 3 for v in versions)
133
+ return sorted(
134
+ [v for v in versions if v.prefix != 'PyPy'] +
135
+ ([Version.from_string('PyPy')] if supports_py2 else []) +
136
+ ([Version.from_string('PyPy3')] if supports_py3 else [])
137
+ )
138
+
139
+
140
+ def update_version_list(
141
+ versions: VersionList,
142
+ add: Optional[VersionList] = None,
143
+ drop: Optional[VersionList] = None,
144
+ update: Optional[VersionList] = None,
145
+ ) -> SortedVersionList:
146
+ """Compute a new list of supported versions.
147
+
148
+ ``add`` will add to supported versions.
149
+ ``drop`` will remove from supported versions.
150
+ ``update`` will specify supported versions.
151
+
152
+ You may combine ``add`` and ``drop``. It doesn't make sense to combine
153
+ ``update`` with either ``add`` or ``drop``.
154
+ """
155
+ if update:
156
+ return sorted(update)
157
+ else:
158
+ return sorted(set(versions).union(add or ()).difference(drop or ()))