-
-
Notifications
You must be signed in to change notification settings - Fork 13
Expand file tree
/
Copy pathversions.py
More file actions
161 lines (127 loc) · 5.07 KB
/
versions.py
File metadata and controls
161 lines (127 loc) · 5.07 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
"""Python version business logic."""
import re
from typing import Collection, List, NamedTuple, Optional, Set, Union
#
# Information about Python releases that needs to be constantly updated as
# Python makes new releases.
#
MAX_PYTHON_1_VERSION = 6 # i.e. 1.6
MAX_PYTHON_2_VERSION = 7 # i.e. 2.7
CURRENT_PYTHON_3_VERSION = 11 # i.e. 3.10
MAX_MINOR_FOR_MAJOR = {
1: MAX_PYTHON_1_VERSION,
2: MAX_PYTHON_2_VERSION,
3: CURRENT_PYTHON_3_VERSION,
}
VERSION_RX = re.compile('^([^-0-9]*)([0-9]*)([.][0-9]+)?(.*)$')
class Version(NamedTuple):
"""A simplified Python version number.
Primarily needed so we can sort lists of version numbers correctly, i.e.
2.7, 3.0, 3.1, 3.2, ..., 3.9, 3.10, ...
Can have an optional prefix, e.g. PyPy3.6 is Version(prefix='PyPy',
major=3, minor=6).
Can have an optional suffix, e.g. 3.10-dev is Version(major=3, minor=10,
suffix='-dev').
Any string can be round-tripped to a Version and back via
Version.from_string() and Version.__str__.
"""
prefix: str = ''
major: int = -1 # I'd've preferred to use None, but it complicates sorting
minor: int = -1
suffix: str = ''
has_dot: bool = False
@classmethod
def from_string(cls, v: str, has_dot: bool = False) -> 'Version':
m = VERSION_RX.match(v)
assert m is not None
prefix, major, minor, suffix = m.groups()
return cls(
prefix,
int(major) if major else -1,
int(minor[1:]) if minor else -1,
suffix,
has_dot,
)
def __repr__(self) -> str:
return 'Version({})'.format(', '.join(part for part in [
f'prefix={self.prefix!r}' if self.prefix else '',
f'major={self.major!r}' if self.major != -1 else '',
f'minor={self.minor!r}' if self.minor != -1 else '',
f'suffix={self.suffix!r}' if self.suffix else '',
f'dot={self.has_dot!r}' if self.has_dot else '',
] if part))
def __str__(self) -> str:
major = '' if self.major == -1 else f'{self.major}'
minor = '' if self.minor == -1 else f'.{self.minor}'
return f'{self.prefix}{major}{minor}{self.suffix}'
VersionSet = Set[Version]
VersionList = Collection[Version]
SortedVersionList = List[Version]
def is_important(v: Union[Version, str]) -> bool:
"""Is the version important for matching purposes?
Different sources can express support for different versions, e.g.
classifiers can express support for "PyPy" but python_requires can't.
Also some CI systems allow testing on unreleased Python versions that
cannot be listed in classifiers, so their presence should not cause
mismatch errors.
"""
if not isinstance(v, Version):
v = Version.from_string(v)
upcoming_release = Version(major=3, minor=CURRENT_PYTHON_3_VERSION + 1)
return (
not v.prefix.startswith(('PyPy', 'Jython')) and v.prefix != 'nightly'
and '-dev' not in v.suffix
and '-alpha' not in v.suffix
and '-beta' not in v.suffix
and '-rc' not in v.suffix
and v != upcoming_release
)
def important(versions: Collection[Version]) -> VersionSet:
"""Filter out unimportant versions.
See `is_important` for what consitutes "important".
"""
return {
v for v in versions
if is_important(v)
}
def pypy_versions(versions: Collection[Version]) -> VersionSet:
"""Filter PyPy versions."""
return {
v for v in versions
if v.prefix.startswith('PyPy')
}
def expand_pypy(versions: Collection[Version]) -> SortedVersionList:
"""Determine whether PyPy support means PyPy2 or PyPy3 or both.
Some data sources (like setup.py classifiers) allow you to indicate PyPy
support without specifying whether you mean PyPy2 or PyPy3. Other data
sources (like all CI systems) are more explicit. To make these version
lists directly comparable we need to look at supported CPython versions and
translate that knowledge into PyPy versions.
"""
supports_pypy = any(v.prefix == 'PyPy' for v in versions)
if not supports_pypy:
return sorted(versions)
supports_py2 = any(v.major == 2 for v in versions)
supports_py3 = any(v.major == 3 for v in versions)
return sorted(
[v for v in versions if v.prefix != 'PyPy'] +
([Version.from_string('PyPy')] if supports_py2 else []) +
([Version.from_string('PyPy3')] if supports_py3 else [])
)
def update_version_list(
versions: VersionList,
add: Optional[VersionList] = None,
drop: Optional[VersionList] = None,
update: Optional[VersionList] = None,
) -> SortedVersionList:
"""Compute a new list of supported versions.
``add`` will add to supported versions.
``drop`` will remove from supported versions.
``update`` will specify supported versions.
You may combine ``add`` and ``drop``. It doesn't make sense to combine
``update`` with either ``add`` or ``drop``.
"""
if update:
return sorted(update)
else:
return sorted(set(versions).union(add or ()).difference(drop or ()))