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.
- check_python_versions/__init__.py +16 -0
- check_python_versions/__main__.py +7 -0
- check_python_versions/cli.py +479 -0
- check_python_versions/parsers/__init__.py +1 -0
- check_python_versions/parsers/classifiers.py +81 -0
- check_python_versions/parsers/ini.py +81 -0
- check_python_versions/parsers/poetry_version_spec.py +276 -0
- check_python_versions/parsers/python.py +262 -0
- check_python_versions/parsers/requires_python.py +239 -0
- check_python_versions/parsers/yaml.py +221 -0
- check_python_versions/sources/__init__.py +1 -0
- check_python_versions/sources/all.py +23 -0
- check_python_versions/sources/appveyor.py +191 -0
- check_python_versions/sources/base.py +87 -0
- check_python_versions/sources/github.py +162 -0
- check_python_versions/sources/manylinux.py +96 -0
- check_python_versions/sources/pyproject.py +244 -0
- check_python_versions/sources/setup_py.py +201 -0
- check_python_versions/sources/tox.py +265 -0
- check_python_versions/sources/travis.py +213 -0
- check_python_versions/utils.py +142 -0
- check_python_versions/versions.py +158 -0
- check_python_versions-0.23.0.dist-info/METADATA +434 -0
- check_python_versions-0.23.0.dist-info/RECORD +28 -0
- check_python_versions-0.23.0.dist-info/WHEEL +5 -0
- check_python_versions-0.23.0.dist-info/entry_points.txt +2 -0
- check_python_versions-0.23.0.dist-info/licenses/LICENSE +674 -0
- check_python_versions-0.23.0.dist-info/top_level.txt +1 -0
@@ -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 ()))
|