nab-python 0.0.1__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.
- nab_python/__init__.py +1 -0
- nab_python/_build/__init__.py +1 -0
- nab_python/_build/env.py +364 -0
- nab_python/_build/errors.py +17 -0
- nab_python/_build/runner.py +254 -0
- nab_python/_lockfile/__init__.py +1 -0
- nab_python/_lockfile/builder.py +339 -0
- nab_python/_lockfile/disjointness.py +207 -0
- nab_python/_lockfile/pylock.py +323 -0
- nab_python/_lockfile/requirements.py +121 -0
- nab_python/_packaging_provider.py +98 -0
- nab_python/_provider/__init__.py +1 -0
- nab_python/_provider/build_remote.py +95 -0
- nab_python/_provider/extras.py +231 -0
- nab_python/_provider/listing.py +442 -0
- nab_python/_provider/lookahead.py +156 -0
- nab_python/_provider/metadata_resolver.py +450 -0
- nab_python/_provider/priority.py +174 -0
- nab_python/_provider/sources.py +215 -0
- nab_python/_testing/__init__.py +1 -0
- nab_python/_testing/coordinator_fake.py +240 -0
- nab_python/_vcs_admission.py +209 -0
- nab_python/_vendor/__init__.py +6 -0
- nab_python/_vendor/packaging/LICENSE +3 -0
- nab_python/_vendor/packaging/LICENSE.APACHE +177 -0
- nab_python/_vendor/packaging/LICENSE.BSD +23 -0
- nab_python/_vendor/packaging/PROVENANCE.md +73 -0
- nab_python/_vendor/packaging/__init__.py +15 -0
- nab_python/_vendor/packaging/_elffile.py +108 -0
- nab_python/_vendor/packaging/_manylinux.py +265 -0
- nab_python/_vendor/packaging/_musllinux.py +88 -0
- nab_python/_vendor/packaging/_parser.py +394 -0
- nab_python/_vendor/packaging/_structures.py +33 -0
- nab_python/_vendor/packaging/_tokenizer.py +196 -0
- nab_python/_vendor/packaging/dependency_groups.py +302 -0
- nab_python/_vendor/packaging/direct_url.py +325 -0
- nab_python/_vendor/packaging/errors.py +94 -0
- nab_python/_vendor/packaging/licenses/__init__.py +186 -0
- nab_python/_vendor/packaging/licenses/_spdx.py +799 -0
- nab_python/_vendor/packaging/markers.py +506 -0
- nab_python/_vendor/packaging/metadata.py +964 -0
- nab_python/_vendor/packaging/py.typed +0 -0
- nab_python/_vendor/packaging/pylock.py +910 -0
- nab_python/_vendor/packaging/ranges.py +1803 -0
- nab_python/_vendor/packaging/requirements.py +132 -0
- nab_python/_vendor/packaging/specifiers.py +1141 -0
- nab_python/_vendor/packaging/tags.py +929 -0
- nab_python/_vendor/packaging/utils.py +296 -0
- nab_python/_vendor/packaging/version.py +1230 -0
- nab_python/build_backend.py +184 -0
- nab_python/config.py +805 -0
- nab_python/download.py +170 -0
- nab_python/fetch.py +827 -0
- nab_python/lockfile.py +238 -0
- nab_python/metadata.py +145 -0
- nab_python/provider.py +1235 -0
- nab_python/py.typed +0 -0
- nab_python/requirements_file.py +180 -0
- nab_python/resolve.py +497 -0
- nab_python/universal/__init__.py +1 -0
- nab_python/universal/matrix.py +235 -0
- nab_python/universal/provider.py +214 -0
- nab_python/universal/reresolve.py +310 -0
- nab_python/universal/resolve.py +508 -0
- nab_python/universal/validate.py +439 -0
- nab_python/universal/wheel_selection.py +327 -0
- nab_python/workspace.py +214 -0
- nab_python-0.0.1.dist-info/METADATA +49 -0
- nab_python-0.0.1.dist-info/RECORD +71 -0
- nab_python-0.0.1.dist-info/WHEEL +4 -0
- nab_python-0.0.1.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,1803 @@
|
|
|
1
|
+
# This file is dual licensed under the terms of the Apache License, Version
|
|
2
|
+
# 2.0, and the BSD License. See the LICENSE file in the root of this repository
|
|
3
|
+
# for complete details.
|
|
4
|
+
"""Public :class:`VersionRange` API and supporting range helpers.
|
|
5
|
+
|
|
6
|
+
The :class:`VersionRange` class exposes a set-algebra view of the
|
|
7
|
+
versions accepted by a :class:`~packaging.specifiers.Specifier` or
|
|
8
|
+
:class:`~packaging.specifiers.SpecifierSet`. Private helpers in this
|
|
9
|
+
module also drive the range-filter hot path used by
|
|
10
|
+
:meth:`Specifier.contains` / :meth:`Specifier.filter` and
|
|
11
|
+
:meth:`SpecifierSet.contains` / :meth:`SpecifierSet.filter`.
|
|
12
|
+
|
|
13
|
+
.. testsetup::
|
|
14
|
+
|
|
15
|
+
from packaging.ranges import VersionRange
|
|
16
|
+
from packaging.specifiers import Specifier, SpecifierSet
|
|
17
|
+
from packaging.version import Version
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
import enum
|
|
23
|
+
import functools
|
|
24
|
+
import typing
|
|
25
|
+
from typing import (
|
|
26
|
+
TYPE_CHECKING,
|
|
27
|
+
Any,
|
|
28
|
+
Final,
|
|
29
|
+
Union,
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
from .version import InvalidVersion, Version
|
|
33
|
+
|
|
34
|
+
if TYPE_CHECKING:
|
|
35
|
+
from collections.abc import Callable, Iterable, Iterator, Sequence
|
|
36
|
+
|
|
37
|
+
from .specifiers import Specifier, SpecifierSet
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
__all__ = ["VersionRange"]
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def __dir__() -> list[str]:
|
|
44
|
+
return __all__
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
#: The smallest possible PEP 440 version. No valid version is less than this.
|
|
48
|
+
_MIN_VERSION: Final[Version] = Version("0.dev0")
|
|
49
|
+
|
|
50
|
+
#: Packed pickle form of a single bound: ``(version_str_or_None,
|
|
51
|
+
#: inclusive, kind_or_None)``. Uses only strings, bools, and ``None``
|
|
52
|
+
#: so the format stays stable across packaging releases.
|
|
53
|
+
_PackedBound = tuple[Union[str, None], bool, Union[str, None]]
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _trim_release(release: tuple[int, ...]) -> tuple[int, ...]:
|
|
57
|
+
"""Strip trailing zeros from a release tuple."""
|
|
58
|
+
end = len(release)
|
|
59
|
+
while end > 1 and release[end - 1] == 0:
|
|
60
|
+
end -= 1
|
|
61
|
+
return release if end == len(release) else release[:end]
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _next_prefix_dev0(version: Version) -> Version:
|
|
65
|
+
"""Smallest version in the next prefix: ``1.2 -> 1.3.dev0``."""
|
|
66
|
+
release = (*version.release[:-1], version.release[-1] + 1)
|
|
67
|
+
return Version.from_parts(epoch=version.epoch, release=release, dev=0)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _base_dev0(version: Version) -> Version:
|
|
71
|
+
"""The ``.dev0`` of a version's base release: ``1.2 -> 1.2.dev0``."""
|
|
72
|
+
return Version.from_parts(epoch=version.epoch, release=version.release, dev=0)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _coerce_version(version: Version | str) -> Version | None:
|
|
76
|
+
"""Parse *version*; ``None`` if invalid."""
|
|
77
|
+
if not isinstance(version, Version):
|
|
78
|
+
try:
|
|
79
|
+
version = Version(version)
|
|
80
|
+
except InvalidVersion:
|
|
81
|
+
return None
|
|
82
|
+
return version
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
class _BoundaryKind(enum.Enum):
|
|
86
|
+
"""Where a boundary marker sits in the version ordering."""
|
|
87
|
+
|
|
88
|
+
AFTER_LOCALS = enum.auto() # after V+local, before V.post0
|
|
89
|
+
AFTER_POSTS = enum.auto() # after V.postN, before next release
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
@functools.total_ordering
|
|
93
|
+
class _BoundaryVersion:
|
|
94
|
+
"""A synthetic point between two real PEP 440 versions.
|
|
95
|
+
|
|
96
|
+
PEP 440 specifier semantics imply boundaries between real versions
|
|
97
|
+
(``<=1.0`` includes ``1.0+local``; ``>1.0`` excludes ``1.0.post0``).
|
|
98
|
+
Relative to a base version V::
|
|
99
|
+
|
|
100
|
+
V < V+local < AFTER_LOCALS(V) < V.post0 < AFTER_POSTS(V)
|
|
101
|
+
|
|
102
|
+
AFTER_LOCALS is the upper bound of ``<=V``, ``==V``, ``!=V`` (no
|
|
103
|
+
local), and the lower bound of the upper-side range of ``!=V``.
|
|
104
|
+
AFTER_POSTS is the lower bound of ``>V`` (V final or pre-release),
|
|
105
|
+
excluding V's post-releases per PEP 440.
|
|
106
|
+
"""
|
|
107
|
+
|
|
108
|
+
__slots__ = (
|
|
109
|
+
"_cached_dev",
|
|
110
|
+
"_cached_epoch",
|
|
111
|
+
"_cached_post",
|
|
112
|
+
"_cached_pre",
|
|
113
|
+
"_cached_trimmed_release",
|
|
114
|
+
"_kind",
|
|
115
|
+
"version",
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
def __init__(self, version: Version, kind: _BoundaryKind) -> None:
|
|
119
|
+
self.version = version
|
|
120
|
+
self._kind = kind
|
|
121
|
+
self._cached_trimmed_release = _trim_release(version.release)
|
|
122
|
+
self._cached_epoch = version.epoch
|
|
123
|
+
self._cached_pre = version.pre
|
|
124
|
+
self._cached_post = version.post
|
|
125
|
+
self._cached_dev = version.dev
|
|
126
|
+
|
|
127
|
+
def _is_family(self, other: Version) -> bool:
|
|
128
|
+
"""Is ``other`` a version that this boundary sorts above?"""
|
|
129
|
+
if other.epoch != self._cached_epoch:
|
|
130
|
+
return False
|
|
131
|
+
# Inline release-trim comparison: other.release matches the
|
|
132
|
+
# trimmed release iff its leading slice is equal and any extra
|
|
133
|
+
# components are zero. Avoids _trim_release's tuple allocation.
|
|
134
|
+
other_release = other.release
|
|
135
|
+
trimmed_release = self._cached_trimmed_release
|
|
136
|
+
trimmed_length = len(trimmed_release)
|
|
137
|
+
if len(other_release) < trimmed_length:
|
|
138
|
+
return False
|
|
139
|
+
if other_release[:trimmed_length] != trimmed_release:
|
|
140
|
+
return False
|
|
141
|
+
for i in range(trimmed_length, len(other_release)):
|
|
142
|
+
if other_release[i] != 0:
|
|
143
|
+
return False
|
|
144
|
+
if other.pre != self._cached_pre:
|
|
145
|
+
return False
|
|
146
|
+
if self._kind == _BoundaryKind.AFTER_LOCALS:
|
|
147
|
+
# Local family: same public version, any local label.
|
|
148
|
+
return other.post == self._cached_post and other.dev == self._cached_dev
|
|
149
|
+
# Post family: V itself + any post-release of V.
|
|
150
|
+
return other.dev == self._cached_dev or other.post is not None
|
|
151
|
+
|
|
152
|
+
def __eq__(self, other: object) -> bool:
|
|
153
|
+
if isinstance(other, _BoundaryVersion):
|
|
154
|
+
return self.version == other.version and self._kind == other._kind
|
|
155
|
+
return NotImplemented
|
|
156
|
+
|
|
157
|
+
def __lt__(self, other: _BoundaryVersion | Version) -> bool:
|
|
158
|
+
if isinstance(other, _BoundaryVersion):
|
|
159
|
+
if self.version != other.version:
|
|
160
|
+
return self.version < other.version
|
|
161
|
+
return self._kind.value < other._kind.value # pragma: no cover
|
|
162
|
+
# boundary < other_version iff V < other AND other not in family.
|
|
163
|
+
# The cheap V >= other path short-circuits before the family check.
|
|
164
|
+
if not (self.version < other):
|
|
165
|
+
return False
|
|
166
|
+
return not self._is_family(other)
|
|
167
|
+
|
|
168
|
+
def __gt__(self, other: _BoundaryVersion | Version) -> bool:
|
|
169
|
+
# Defined directly to bypass functools.total_ordering's
|
|
170
|
+
# NotImplemented round-trip on reflected ``Version < boundary``.
|
|
171
|
+
if isinstance(other, _BoundaryVersion):
|
|
172
|
+
if self.version != other.version:
|
|
173
|
+
return self.version > other.version
|
|
174
|
+
return self._kind.value > other._kind.value
|
|
175
|
+
if self.version >= other:
|
|
176
|
+
return True
|
|
177
|
+
return self._is_family(other)
|
|
178
|
+
|
|
179
|
+
def __hash__(self) -> int:
|
|
180
|
+
return hash((self.version, self._kind))
|
|
181
|
+
|
|
182
|
+
def __repr__(self) -> str:
|
|
183
|
+
return f"{self.__class__.__name__}({self.version!r}, {self._kind.name})"
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
if TYPE_CHECKING:
|
|
187
|
+
_VersionOrBoundary = Union[Version, _BoundaryVersion, None]
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def _make_above_after_posts(version: Version) -> Callable[[Version], bool]:
|
|
191
|
+
"""Predicate ``parsed > AFTER_POSTS(v)`` for a lower bound.
|
|
192
|
+
|
|
193
|
+
Per PEP 440, ``>V`` excludes V's post-releases unless V is itself
|
|
194
|
+
a post-release. AFTER_POSTS sits above V and every V.postN (with
|
|
195
|
+
or without local), and just below the next release.
|
|
196
|
+
"""
|
|
197
|
+
version_ge = version.__ge__
|
|
198
|
+
version_epoch = version.epoch
|
|
199
|
+
version_pre = version.pre
|
|
200
|
+
version_dev = version.dev
|
|
201
|
+
version_release_trimmed = _trim_release(version.release)
|
|
202
|
+
trimmed_length = len(version_release_trimmed)
|
|
203
|
+
|
|
204
|
+
def above(parsed: Version) -> bool:
|
|
205
|
+
if version_ge(parsed):
|
|
206
|
+
return False
|
|
207
|
+
# parsed > v cmpkey-wise: above the boundary iff NOT in v's
|
|
208
|
+
# post family.
|
|
209
|
+
if parsed.epoch != version_epoch:
|
|
210
|
+
return True
|
|
211
|
+
parsed_release = parsed.release
|
|
212
|
+
if len(parsed_release) < trimmed_length:
|
|
213
|
+
return True
|
|
214
|
+
if parsed_release[:trimmed_length] != version_release_trimmed:
|
|
215
|
+
return True
|
|
216
|
+
for i in range(trimmed_length, len(parsed_release)):
|
|
217
|
+
if parsed_release[i] != 0:
|
|
218
|
+
return True
|
|
219
|
+
if parsed.pre != version_pre:
|
|
220
|
+
return True
|
|
221
|
+
# In post family iff: same dev as v (covers v itself + v+local),
|
|
222
|
+
# or any post-release (covers v.postN + v.postN+local).
|
|
223
|
+
if parsed.dev == version_dev or parsed.post is not None:
|
|
224
|
+
return False
|
|
225
|
+
# Different dev with no post means parsed sorts before v
|
|
226
|
+
# cmpkey-wise, in which case version_ge returned True already.
|
|
227
|
+
return False # pragma: no cover
|
|
228
|
+
|
|
229
|
+
return above
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def _make_above_after_locals(version: Version) -> Callable[[Version], bool]:
|
|
233
|
+
"""Predicate ``parsed > AFTER_LOCALS(v)`` for a lower bound.
|
|
234
|
+
|
|
235
|
+
Used by the upper-side range of ``!=v`` (when *v* has no local
|
|
236
|
+
segment). AFTER_LOCALS sits above v and every ``v+local`` but
|
|
237
|
+
just below ``v.post0``.
|
|
238
|
+
"""
|
|
239
|
+
version_ge = version.__ge__
|
|
240
|
+
version_epoch = version.epoch
|
|
241
|
+
version_pre = version.pre
|
|
242
|
+
version_post = version.post
|
|
243
|
+
version_dev = version.dev
|
|
244
|
+
version_release_trimmed = _trim_release(version.release)
|
|
245
|
+
trimmed_length = len(version_release_trimmed)
|
|
246
|
+
|
|
247
|
+
def above(parsed: Version) -> bool:
|
|
248
|
+
if version_ge(parsed):
|
|
249
|
+
return False
|
|
250
|
+
# parsed > v cmpkey-wise: above the boundary iff NOT in v's
|
|
251
|
+
# local family (same public version, any local segment).
|
|
252
|
+
if parsed.epoch != version_epoch:
|
|
253
|
+
return True
|
|
254
|
+
parsed_release = parsed.release
|
|
255
|
+
if len(parsed_release) < trimmed_length:
|
|
256
|
+
return True
|
|
257
|
+
if parsed_release[:trimmed_length] != version_release_trimmed:
|
|
258
|
+
return True
|
|
259
|
+
for i in range(trimmed_length, len(parsed_release)):
|
|
260
|
+
if parsed_release[i] != 0:
|
|
261
|
+
return True
|
|
262
|
+
if parsed.pre != version_pre:
|
|
263
|
+
return True
|
|
264
|
+
if parsed.post != version_post:
|
|
265
|
+
return True
|
|
266
|
+
return parsed.dev != version_dev
|
|
267
|
+
|
|
268
|
+
return above
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
def _make_below_after_locals(version: Version) -> Callable[[Version], bool]:
|
|
272
|
+
"""Predicate ``parsed <= AFTER_LOCALS(v)`` for an upper bound.
|
|
273
|
+
|
|
274
|
+
Used by ``<=v``, ``==v``, ``!=v`` (no local). ``parsed`` is at or
|
|
275
|
+
below the boundary when it is at or below v cmpkey-wise, or when
|
|
276
|
+
it is in v's local family.
|
|
277
|
+
"""
|
|
278
|
+
version_ge = version.__ge__
|
|
279
|
+
version_epoch = version.epoch
|
|
280
|
+
version_pre = version.pre
|
|
281
|
+
version_post = version.post
|
|
282
|
+
version_dev = version.dev
|
|
283
|
+
version_release_trimmed = _trim_release(version.release)
|
|
284
|
+
trimmed_length = len(version_release_trimmed)
|
|
285
|
+
|
|
286
|
+
def below(parsed: Version) -> bool:
|
|
287
|
+
if version_ge(parsed):
|
|
288
|
+
return True
|
|
289
|
+
# parsed > v cmpkey-wise: below the boundary iff in v's local
|
|
290
|
+
# family.
|
|
291
|
+
if parsed.epoch != version_epoch:
|
|
292
|
+
return False
|
|
293
|
+
parsed_release = parsed.release
|
|
294
|
+
if len(parsed_release) < trimmed_length:
|
|
295
|
+
return False
|
|
296
|
+
if parsed_release[:trimmed_length] != version_release_trimmed:
|
|
297
|
+
return False
|
|
298
|
+
for i in range(trimmed_length, len(parsed_release)):
|
|
299
|
+
if parsed_release[i] != 0:
|
|
300
|
+
return False
|
|
301
|
+
if parsed.pre != version_pre:
|
|
302
|
+
return False
|
|
303
|
+
if parsed.post != version_post:
|
|
304
|
+
return False
|
|
305
|
+
return parsed.dev == version_dev
|
|
306
|
+
|
|
307
|
+
return below
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
def _make_below_after_posts(version: Version) -> Callable[[Version], bool]:
|
|
311
|
+
"""Predicate ``parsed <= AFTER_POSTS(v)`` for an upper bound.
|
|
312
|
+
|
|
313
|
+
Mirror of :func:`_make_above_after_posts`. Produced only by
|
|
314
|
+
:meth:`VersionRange.complement` of a range whose lower bound is
|
|
315
|
+
AFTER_POSTS(v). ``parsed`` is at or below the boundary when it is
|
|
316
|
+
at or below v cmpkey-wise, or when it is in v's post family.
|
|
317
|
+
"""
|
|
318
|
+
version_ge = version.__ge__
|
|
319
|
+
version_epoch = version.epoch
|
|
320
|
+
version_pre = version.pre
|
|
321
|
+
version_dev = version.dev
|
|
322
|
+
version_release_trimmed = _trim_release(version.release)
|
|
323
|
+
trimmed_length = len(version_release_trimmed)
|
|
324
|
+
|
|
325
|
+
def below(parsed: Version) -> bool:
|
|
326
|
+
if version_ge(parsed):
|
|
327
|
+
return True
|
|
328
|
+
# parsed > v cmpkey-wise: below the boundary iff in v's post family.
|
|
329
|
+
if parsed.epoch != version_epoch:
|
|
330
|
+
return False
|
|
331
|
+
parsed_release = parsed.release
|
|
332
|
+
if len(parsed_release) < trimmed_length:
|
|
333
|
+
return False
|
|
334
|
+
if parsed_release[:trimmed_length] != version_release_trimmed:
|
|
335
|
+
return False
|
|
336
|
+
for i in range(trimmed_length, len(parsed_release)):
|
|
337
|
+
if parsed_release[i] != 0:
|
|
338
|
+
return False
|
|
339
|
+
if parsed.pre != version_pre:
|
|
340
|
+
return False
|
|
341
|
+
# Same dev as v with no post means parsed sorts <= v already
|
|
342
|
+
# (handled by version_ge above); reach here only with parsed.post set.
|
|
343
|
+
return parsed.dev == version_dev or parsed.post is not None
|
|
344
|
+
|
|
345
|
+
return below
|
|
346
|
+
|
|
347
|
+
|
|
348
|
+
@functools.total_ordering
|
|
349
|
+
class _LowerBound:
|
|
350
|
+
"""Lower bound of a version range.
|
|
351
|
+
|
|
352
|
+
A ``version`` of ``None`` is unbounded below (-inf). At equal
|
|
353
|
+
versions, ``[v`` sorts before ``(v`` (inclusive starts earlier).
|
|
354
|
+
"""
|
|
355
|
+
|
|
356
|
+
__slots__ = ("_above", "inclusive", "version")
|
|
357
|
+
|
|
358
|
+
def __init__(self, version: _VersionOrBoundary, inclusive: bool) -> None:
|
|
359
|
+
self.version = version
|
|
360
|
+
self.inclusive = inclusive
|
|
361
|
+
# Pre-bind a predicate "is parsed at or above this lower
|
|
362
|
+
# bound?" for the hot filter / contains loops. One direct
|
|
363
|
+
# call per check, no operator-dispatch chain.
|
|
364
|
+
if version is None:
|
|
365
|
+
self._above: Callable[[Version], bool] | None = None
|
|
366
|
+
elif isinstance(version, _BoundaryVersion):
|
|
367
|
+
# >v produces an AFTER_POSTS lower bound; the upper-side
|
|
368
|
+
# range of !=v produces an AFTER_LOCALS lower bound.
|
|
369
|
+
if version._kind == _BoundaryKind.AFTER_POSTS:
|
|
370
|
+
self._above = _make_above_after_posts(version.version)
|
|
371
|
+
else:
|
|
372
|
+
self._above = _make_above_after_locals(version.version)
|
|
373
|
+
elif inclusive:
|
|
374
|
+
self._above = version.__le__
|
|
375
|
+
else:
|
|
376
|
+
self._above = version.__lt__
|
|
377
|
+
|
|
378
|
+
def __eq__(self, other: object) -> bool:
|
|
379
|
+
if not isinstance(other, _LowerBound):
|
|
380
|
+
return NotImplemented # pragma: no cover
|
|
381
|
+
return self.version == other.version and self.inclusive == other.inclusive
|
|
382
|
+
|
|
383
|
+
def __lt__(self, other: _LowerBound) -> bool:
|
|
384
|
+
if not isinstance(other, _LowerBound): # pragma: no cover
|
|
385
|
+
return NotImplemented
|
|
386
|
+
# -inf < anything (except -inf itself).
|
|
387
|
+
if self.version is None:
|
|
388
|
+
return other.version is not None
|
|
389
|
+
if other.version is None:
|
|
390
|
+
return False
|
|
391
|
+
if self.version != other.version:
|
|
392
|
+
return self.version < other.version
|
|
393
|
+
# ``[v < (v``: inclusive starts earlier.
|
|
394
|
+
return self.inclusive and not other.inclusive
|
|
395
|
+
|
|
396
|
+
def __hash__(self) -> int:
|
|
397
|
+
return hash((self.version, self.inclusive))
|
|
398
|
+
|
|
399
|
+
def __repr__(self) -> str:
|
|
400
|
+
bracket = "[" if self.inclusive else "("
|
|
401
|
+
return f"<{self.__class__.__name__} {bracket}{self.version!r}>"
|
|
402
|
+
|
|
403
|
+
|
|
404
|
+
@functools.total_ordering
|
|
405
|
+
class _UpperBound:
|
|
406
|
+
"""Upper bound of a version range.
|
|
407
|
+
|
|
408
|
+
A ``version`` of ``None`` is unbounded above (+inf). At equal
|
|
409
|
+
versions, ``v)`` sorts before ``v]`` (exclusive ends earlier).
|
|
410
|
+
"""
|
|
411
|
+
|
|
412
|
+
__slots__ = ("_below", "inclusive", "version")
|
|
413
|
+
|
|
414
|
+
def __init__(self, version: _VersionOrBoundary, inclusive: bool) -> None:
|
|
415
|
+
self.version = version
|
|
416
|
+
self.inclusive = inclusive
|
|
417
|
+
# Pre-bind a predicate "is parsed at or below this upper
|
|
418
|
+
# bound?". See _LowerBound for the rationale.
|
|
419
|
+
if version is None:
|
|
420
|
+
self._below: Callable[[Version], bool] | None = None
|
|
421
|
+
elif isinstance(version, _BoundaryVersion):
|
|
422
|
+
# Standard specifiers only ever produce AFTER_LOCALS upper
|
|
423
|
+
# bounds (from <=v / ==v / !=v with no local). Complement
|
|
424
|
+
# reverses bound roles, so a range whose lower bound is
|
|
425
|
+
# AFTER_POSTS(v) becomes an upper bound after complementing.
|
|
426
|
+
if version._kind == _BoundaryKind.AFTER_LOCALS:
|
|
427
|
+
self._below = _make_below_after_locals(version.version)
|
|
428
|
+
else:
|
|
429
|
+
self._below = _make_below_after_posts(version.version)
|
|
430
|
+
elif inclusive:
|
|
431
|
+
self._below = version.__ge__
|
|
432
|
+
else:
|
|
433
|
+
self._below = version.__gt__
|
|
434
|
+
|
|
435
|
+
def __eq__(self, other: object) -> bool:
|
|
436
|
+
if not isinstance(other, _UpperBound):
|
|
437
|
+
return NotImplemented # pragma: no cover
|
|
438
|
+
return self.version == other.version and self.inclusive == other.inclusive
|
|
439
|
+
|
|
440
|
+
def __lt__(self, other: _UpperBound) -> bool:
|
|
441
|
+
if not isinstance(other, _UpperBound): # pragma: no cover
|
|
442
|
+
return NotImplemented
|
|
443
|
+
# Nothing < +inf (except +inf itself).
|
|
444
|
+
if self.version is None:
|
|
445
|
+
return False
|
|
446
|
+
if other.version is None:
|
|
447
|
+
return True
|
|
448
|
+
if self.version != other.version:
|
|
449
|
+
return self.version < other.version
|
|
450
|
+
# ``v) < v]``: exclusive ends earlier.
|
|
451
|
+
return not self.inclusive and other.inclusive
|
|
452
|
+
|
|
453
|
+
def __hash__(self) -> int:
|
|
454
|
+
return hash((self.version, self.inclusive))
|
|
455
|
+
|
|
456
|
+
def __repr__(self) -> str:
|
|
457
|
+
bracket = "]" if self.inclusive else ")"
|
|
458
|
+
return f"<{self.__class__.__name__} {self.version!r}{bracket}>"
|
|
459
|
+
|
|
460
|
+
|
|
461
|
+
if TYPE_CHECKING:
|
|
462
|
+
#: A single contiguous version range, as a (lower, upper) pair.
|
|
463
|
+
_VersionRange = tuple[_LowerBound, _UpperBound]
|
|
464
|
+
|
|
465
|
+
|
|
466
|
+
_NEG_INF = _LowerBound(None, False)
|
|
467
|
+
_POS_INF = _UpperBound(None, False)
|
|
468
|
+
_FULL_RANGE: tuple[_VersionRange] = ((_NEG_INF, _POS_INF),)
|
|
469
|
+
|
|
470
|
+
|
|
471
|
+
def _range_is_empty(lower: _LowerBound, upper: _UpperBound) -> bool:
|
|
472
|
+
"""True when the range ``(lower, upper)`` contains no versions."""
|
|
473
|
+
if lower.version is None or upper.version is None:
|
|
474
|
+
return False
|
|
475
|
+
if lower.version == upper.version:
|
|
476
|
+
return not (lower.inclusive and upper.inclusive)
|
|
477
|
+
return lower.version > upper.version
|
|
478
|
+
|
|
479
|
+
|
|
480
|
+
def _intersect_ranges(
|
|
481
|
+
left: Sequence[_VersionRange],
|
|
482
|
+
right: Sequence[_VersionRange],
|
|
483
|
+
) -> list[_VersionRange]:
|
|
484
|
+
"""Intersect two sorted, non-overlapping range lists (two-pointer merge)."""
|
|
485
|
+
result: list[_VersionRange] = []
|
|
486
|
+
left_index = right_index = 0
|
|
487
|
+
while left_index < len(left) and right_index < len(right):
|
|
488
|
+
left_lower, left_upper = left[left_index]
|
|
489
|
+
right_lower, right_upper = right[right_index]
|
|
490
|
+
|
|
491
|
+
lower = max(left_lower, right_lower)
|
|
492
|
+
upper = min(left_upper, right_upper)
|
|
493
|
+
|
|
494
|
+
if not _range_is_empty(lower, upper):
|
|
495
|
+
result.append((lower, upper))
|
|
496
|
+
|
|
497
|
+
# Advance whichever side has the smaller upper bound.
|
|
498
|
+
if left_upper < right_upper:
|
|
499
|
+
left_index += 1
|
|
500
|
+
else:
|
|
501
|
+
right_index += 1
|
|
502
|
+
|
|
503
|
+
return result
|
|
504
|
+
|
|
505
|
+
|
|
506
|
+
def _union_ranges(
|
|
507
|
+
left: Sequence[_VersionRange],
|
|
508
|
+
right: Sequence[_VersionRange],
|
|
509
|
+
) -> list[_VersionRange]:
|
|
510
|
+
"""Union two sorted, non-overlapping range lists.
|
|
511
|
+
|
|
512
|
+
Linear merge over the two pre-sorted inputs followed by a single
|
|
513
|
+
coalescing pass: adjacent or overlapping ranges collapse so the
|
|
514
|
+
result is itself sorted and non-overlapping.
|
|
515
|
+
"""
|
|
516
|
+
if not left:
|
|
517
|
+
return list(right)
|
|
518
|
+
if not right:
|
|
519
|
+
return list(left)
|
|
520
|
+
|
|
521
|
+
# Merge two sorted lists by lower bound (linear, no resort).
|
|
522
|
+
merged_input: list[_VersionRange] = []
|
|
523
|
+
left_index = right_index = 0
|
|
524
|
+
while left_index < len(left) and right_index < len(right):
|
|
525
|
+
if left[left_index][0] <= right[right_index][0]:
|
|
526
|
+
merged_input.append(left[left_index])
|
|
527
|
+
left_index += 1
|
|
528
|
+
else:
|
|
529
|
+
merged_input.append(right[right_index])
|
|
530
|
+
right_index += 1
|
|
531
|
+
merged_input.extend(left[left_index:])
|
|
532
|
+
merged_input.extend(right[right_index:])
|
|
533
|
+
|
|
534
|
+
merged: list[_VersionRange] = [merged_input[0]]
|
|
535
|
+
for lower, upper in merged_input[1:]:
|
|
536
|
+
prev_lower, prev_upper = merged[-1]
|
|
537
|
+
|
|
538
|
+
# Adjacent ranges merge when the previous upper sits at or past
|
|
539
|
+
# the new lower; +inf/-inf short-circuits collapse the
|
|
540
|
+
# unbounded cases.
|
|
541
|
+
if prev_upper.version is None:
|
|
542
|
+
overlaps = True
|
|
543
|
+
elif lower.version is None:
|
|
544
|
+
overlaps = True # pragma: no cover (merged_input sorted by lower)
|
|
545
|
+
elif prev_upper.version > lower.version:
|
|
546
|
+
overlaps = True
|
|
547
|
+
elif prev_upper.version == lower.version:
|
|
548
|
+
overlaps = prev_upper.inclusive or lower.inclusive
|
|
549
|
+
else:
|
|
550
|
+
overlaps = False
|
|
551
|
+
|
|
552
|
+
if overlaps:
|
|
553
|
+
new_upper = max(prev_upper, upper)
|
|
554
|
+
merged[-1] = (prev_lower, new_upper)
|
|
555
|
+
else:
|
|
556
|
+
merged.append((lower, upper))
|
|
557
|
+
|
|
558
|
+
return merged
|
|
559
|
+
|
|
560
|
+
|
|
561
|
+
def _complement_ranges(
|
|
562
|
+
ranges: Sequence[_VersionRange],
|
|
563
|
+
) -> list[_VersionRange]:
|
|
564
|
+
"""Complement a sorted, non-overlapping range list.
|
|
565
|
+
|
|
566
|
+
Yields the gaps between ranges plus a leading gap before the first
|
|
567
|
+
range and a trailing gap after the last. Bound inclusivity flips
|
|
568
|
+
so complement-of-complement round-trips back to the input.
|
|
569
|
+
"""
|
|
570
|
+
if not ranges:
|
|
571
|
+
return list(_FULL_RANGE)
|
|
572
|
+
|
|
573
|
+
result: list[_VersionRange] = []
|
|
574
|
+
prev_upper: _UpperBound | None = None
|
|
575
|
+
|
|
576
|
+
for lower, upper in ranges:
|
|
577
|
+
if prev_upper is None:
|
|
578
|
+
# Leading gap from -inf up to the first range's lower.
|
|
579
|
+
if lower.version is not None:
|
|
580
|
+
gap_upper = _UpperBound(lower.version, not lower.inclusive)
|
|
581
|
+
result.append((_NEG_INF, gap_upper))
|
|
582
|
+
else:
|
|
583
|
+
gap_lower = _LowerBound(prev_upper.version, not prev_upper.inclusive)
|
|
584
|
+
gap_upper = _UpperBound(lower.version, not lower.inclusive)
|
|
585
|
+
# Adjacent ranges in the input are non-touching by
|
|
586
|
+
# construction, so the gap between them is non-empty.
|
|
587
|
+
if not _range_is_empty(gap_lower, gap_upper): # pragma: no branch
|
|
588
|
+
result.append((gap_lower, gap_upper))
|
|
589
|
+
prev_upper = upper
|
|
590
|
+
|
|
591
|
+
# Trailing gap from the final range's upper to +inf.
|
|
592
|
+
if prev_upper is not None and prev_upper.version is not None:
|
|
593
|
+
gap_lower = _LowerBound(prev_upper.version, not prev_upper.inclusive)
|
|
594
|
+
result.append((gap_lower, _POS_INF))
|
|
595
|
+
|
|
596
|
+
return result
|
|
597
|
+
|
|
598
|
+
|
|
599
|
+
def _filter_by_ranges(
|
|
600
|
+
ranges: Sequence[_VersionRange],
|
|
601
|
+
iterable: Iterable[Any],
|
|
602
|
+
key: Callable[[Any], Version | str] | None,
|
|
603
|
+
prereleases: bool | None,
|
|
604
|
+
) -> Iterator[Any]:
|
|
605
|
+
"""Filter *iterable* against precomputed version *ranges*.
|
|
606
|
+
|
|
607
|
+
With ``prereleases=None``, the PEP 440 default applies: pre-releases
|
|
608
|
+
are excluded unless no final matches, in which case buffered
|
|
609
|
+
pre-releases come out at the end.
|
|
610
|
+
"""
|
|
611
|
+
if prereleases is None:
|
|
612
|
+
# PEP 440 default: yield finals immediately; buffer
|
|
613
|
+
# pre-releases until at least one final has been emitted.
|
|
614
|
+
nonfinal_buffer: list[Any] = []
|
|
615
|
+
found_final = False
|
|
616
|
+
|
|
617
|
+
if len(ranges) == 1:
|
|
618
|
+
lower, upper = ranges[0]
|
|
619
|
+
above = lower._above
|
|
620
|
+
below = upper._below
|
|
621
|
+
for item in iterable:
|
|
622
|
+
parsed = _coerce_version(item if key is None else key(item))
|
|
623
|
+
if parsed is None:
|
|
624
|
+
continue
|
|
625
|
+
if above is not None and not above(parsed):
|
|
626
|
+
continue
|
|
627
|
+
if below is not None and not below(parsed):
|
|
628
|
+
continue
|
|
629
|
+
if parsed.is_prerelease:
|
|
630
|
+
if not found_final:
|
|
631
|
+
nonfinal_buffer.append(item)
|
|
632
|
+
else:
|
|
633
|
+
found_final = True
|
|
634
|
+
yield item
|
|
635
|
+
if not found_final:
|
|
636
|
+
yield from nonfinal_buffer
|
|
637
|
+
return
|
|
638
|
+
|
|
639
|
+
for item in iterable:
|
|
640
|
+
parsed = _coerce_version(item if key is None else key(item))
|
|
641
|
+
if parsed is None:
|
|
642
|
+
continue
|
|
643
|
+
for lower, upper in ranges:
|
|
644
|
+
above = lower._above
|
|
645
|
+
if above is not None and not above(parsed):
|
|
646
|
+
break
|
|
647
|
+
below = upper._below
|
|
648
|
+
if below is None or below(parsed):
|
|
649
|
+
if parsed.is_prerelease:
|
|
650
|
+
if not found_final:
|
|
651
|
+
nonfinal_buffer.append(item)
|
|
652
|
+
else:
|
|
653
|
+
found_final = True
|
|
654
|
+
yield item
|
|
655
|
+
break
|
|
656
|
+
if not found_final:
|
|
657
|
+
yield from nonfinal_buffer
|
|
658
|
+
return
|
|
659
|
+
|
|
660
|
+
exclude_prereleases = prereleases is False
|
|
661
|
+
|
|
662
|
+
if len(ranges) == 1:
|
|
663
|
+
# Hot path: most specifiers and small SpecifierSets reduce to
|
|
664
|
+
# a single contiguous range.
|
|
665
|
+
lower, upper = ranges[0]
|
|
666
|
+
above = lower._above
|
|
667
|
+
below = upper._below
|
|
668
|
+
for item in iterable:
|
|
669
|
+
parsed = _coerce_version(item if key is None else key(item))
|
|
670
|
+
if parsed is None:
|
|
671
|
+
continue
|
|
672
|
+
if exclude_prereleases and parsed.is_prerelease:
|
|
673
|
+
continue
|
|
674
|
+
if above is not None and not above(parsed):
|
|
675
|
+
continue
|
|
676
|
+
if below is None or below(parsed):
|
|
677
|
+
yield item
|
|
678
|
+
return
|
|
679
|
+
|
|
680
|
+
for item in iterable:
|
|
681
|
+
parsed = _coerce_version(item if key is None else key(item))
|
|
682
|
+
if parsed is None:
|
|
683
|
+
continue
|
|
684
|
+
if exclude_prereleases and parsed.is_prerelease:
|
|
685
|
+
continue
|
|
686
|
+
for lower, upper in ranges:
|
|
687
|
+
above = lower._above
|
|
688
|
+
if above is not None and not above(parsed):
|
|
689
|
+
break
|
|
690
|
+
below = upper._below
|
|
691
|
+
if below is None or below(parsed):
|
|
692
|
+
yield item
|
|
693
|
+
break
|
|
694
|
+
|
|
695
|
+
|
|
696
|
+
def _matches_bounds_only(
|
|
697
|
+
bounds: Sequence[_VersionRange],
|
|
698
|
+
item: Version,
|
|
699
|
+
) -> bool:
|
|
700
|
+
"""Pure-bounds membership check for a parsed Version."""
|
|
701
|
+
if not bounds:
|
|
702
|
+
return False
|
|
703
|
+
if len(bounds) == 1:
|
|
704
|
+
lower, upper = bounds[0]
|
|
705
|
+
above = lower._above
|
|
706
|
+
if above is not None and not above(item):
|
|
707
|
+
return False
|
|
708
|
+
below = upper._below
|
|
709
|
+
return below is None or below(item)
|
|
710
|
+
for lower, upper in bounds:
|
|
711
|
+
above = lower._above
|
|
712
|
+
if above is not None and not above(item):
|
|
713
|
+
return False
|
|
714
|
+
below = upper._below
|
|
715
|
+
if below is None or below(item):
|
|
716
|
+
return True
|
|
717
|
+
return False
|
|
718
|
+
|
|
719
|
+
|
|
720
|
+
def _bound_match_string(bounds: Sequence[_VersionRange], s: str) -> bool:
|
|
721
|
+
"""Bound-only check for the case-folded string *s*.
|
|
722
|
+
|
|
723
|
+
Full-range bounds admit any string. Other shapes require *s* to
|
|
724
|
+
parse and fall inside the intervals.
|
|
725
|
+
"""
|
|
726
|
+
if tuple(bounds) == _FULL_RANGE:
|
|
727
|
+
return True
|
|
728
|
+
parsed = _coerce_version(s)
|
|
729
|
+
if parsed is None:
|
|
730
|
+
return False
|
|
731
|
+
return _matches_bounds_only(bounds, parsed)
|
|
732
|
+
|
|
733
|
+
|
|
734
|
+
def _lowest_release_at_or_above(
|
|
735
|
+
value: Version | _BoundaryVersion | None,
|
|
736
|
+
) -> Version | None:
|
|
737
|
+
"""Smallest non-pre-release version at or above *value*, or None."""
|
|
738
|
+
if value is None:
|
|
739
|
+
return None
|
|
740
|
+
if isinstance(value, _BoundaryVersion):
|
|
741
|
+
inner_version = value.version
|
|
742
|
+
if inner_version.is_prerelease:
|
|
743
|
+
# AFTER_LOCALS(1.0a1) -> nearest non-pre is 1.0
|
|
744
|
+
return inner_version.__replace__(pre=None, dev=None, local=None)
|
|
745
|
+
# AFTER_LOCALS(1.0) -> nearest non-pre is 1.0.post0
|
|
746
|
+
# AFTER_LOCALS(1.0.post0) -> nearest non-pre is 1.0.post1
|
|
747
|
+
next_post = (inner_version.post + 1) if inner_version.post is not None else 0
|
|
748
|
+
return inner_version.__replace__(post=next_post, local=None)
|
|
749
|
+
if not value.is_prerelease:
|
|
750
|
+
return value
|
|
751
|
+
# Strip pre/dev to get the final or post-release form.
|
|
752
|
+
return value.__replace__(pre=None, dev=None, local=None)
|
|
753
|
+
|
|
754
|
+
|
|
755
|
+
def _ranges_are_prerelease_only(ranges: Sequence[_VersionRange]) -> bool:
|
|
756
|
+
"""``True`` when every range in *ranges* contains only pre-releases.
|
|
757
|
+
|
|
758
|
+
Used to detect unsatisfiable specifier sets when ``prereleases=False``:
|
|
759
|
+
if every range is pre-release-only, every contained version is excluded.
|
|
760
|
+
"""
|
|
761
|
+
for lower, upper in ranges:
|
|
762
|
+
nearest = _lowest_release_at_or_above(lower.version)
|
|
763
|
+
if nearest is None:
|
|
764
|
+
return False
|
|
765
|
+
if upper.version is None or nearest < upper.version:
|
|
766
|
+
return False
|
|
767
|
+
if nearest == upper.version and upper.inclusive:
|
|
768
|
+
return False
|
|
769
|
+
return True
|
|
770
|
+
|
|
771
|
+
|
|
772
|
+
def _wildcard_ranges(op: str, base: Version) -> list[_VersionRange]:
|
|
773
|
+
"""Ranges for ``==V.*`` and ``!=V.*``.
|
|
774
|
+
|
|
775
|
+
``==1.2.*`` -> ``[1.2.dev0, 1.3.dev0)``; ``!=1.2.*`` -> complement.
|
|
776
|
+
"""
|
|
777
|
+
lower = _base_dev0(base)
|
|
778
|
+
upper = _next_prefix_dev0(base)
|
|
779
|
+
if op == "==":
|
|
780
|
+
return [(_LowerBound(lower, True), _UpperBound(upper, False))]
|
|
781
|
+
# !=
|
|
782
|
+
return [
|
|
783
|
+
(_NEG_INF, _UpperBound(lower, False)),
|
|
784
|
+
(_LowerBound(upper, True), _POS_INF),
|
|
785
|
+
]
|
|
786
|
+
|
|
787
|
+
|
|
788
|
+
def _standard_ranges(op: str, version: Version, has_local: bool) -> list[_VersionRange]:
|
|
789
|
+
"""Ranges for the standard PEP 440 operators (no wildcard, no ===).
|
|
790
|
+
|
|
791
|
+
*has_local* indicates whether the spec string included a ``+local``
|
|
792
|
+
segment; relevant only for ``==`` / ``!=`` to decide whether the
|
|
793
|
+
upper bound includes V's local family.
|
|
794
|
+
"""
|
|
795
|
+
if op == ">=":
|
|
796
|
+
return [(_LowerBound(version, True), _POS_INF)]
|
|
797
|
+
|
|
798
|
+
if op == "<=":
|
|
799
|
+
return [
|
|
800
|
+
(
|
|
801
|
+
_NEG_INF,
|
|
802
|
+
_UpperBound(
|
|
803
|
+
_BoundaryVersion(version, _BoundaryKind.AFTER_LOCALS), True
|
|
804
|
+
),
|
|
805
|
+
)
|
|
806
|
+
]
|
|
807
|
+
|
|
808
|
+
if op == ">":
|
|
809
|
+
if version.dev is not None:
|
|
810
|
+
# >V.devN: dev versions have no post-releases, so the
|
|
811
|
+
# next real version is V.dev(N+1).
|
|
812
|
+
lower_bound = version.__replace__(dev=version.dev + 1, local=None)
|
|
813
|
+
return [(_LowerBound(lower_bound, True), _POS_INF)]
|
|
814
|
+
if version.post is not None:
|
|
815
|
+
# >V.postN: next real version is V.post(N+1).dev0.
|
|
816
|
+
lower_bound = version.__replace__(post=version.post + 1, dev=0, local=None)
|
|
817
|
+
return [(_LowerBound(lower_bound, True), _POS_INF)]
|
|
818
|
+
# >V (final or pre-release V): exclude V itself, V+local, and
|
|
819
|
+
# every V.postN per PEP 440.
|
|
820
|
+
return [
|
|
821
|
+
(
|
|
822
|
+
_LowerBound(
|
|
823
|
+
_BoundaryVersion(version, _BoundaryKind.AFTER_POSTS), False
|
|
824
|
+
),
|
|
825
|
+
_POS_INF,
|
|
826
|
+
)
|
|
827
|
+
]
|
|
828
|
+
|
|
829
|
+
if op == "<":
|
|
830
|
+
# <V excludes pre-releases of V when V is not a pre-release.
|
|
831
|
+
# V.dev0 is the earliest pre-release of V.
|
|
832
|
+
bound = (
|
|
833
|
+
version if version.is_prerelease else version.__replace__(dev=0, local=None)
|
|
834
|
+
)
|
|
835
|
+
if bound <= _MIN_VERSION:
|
|
836
|
+
return []
|
|
837
|
+
return [(_NEG_INF, _UpperBound(bound, False))]
|
|
838
|
+
|
|
839
|
+
# ==, !=: local versions of V match when the spec has no local segment.
|
|
840
|
+
after_locals = _BoundaryVersion(version, _BoundaryKind.AFTER_LOCALS)
|
|
841
|
+
upper = version if has_local else after_locals
|
|
842
|
+
|
|
843
|
+
if op == "==":
|
|
844
|
+
return [(_LowerBound(version, True), _UpperBound(upper, True))]
|
|
845
|
+
|
|
846
|
+
if op == "!=":
|
|
847
|
+
return [
|
|
848
|
+
(_NEG_INF, _UpperBound(version, False)),
|
|
849
|
+
(_LowerBound(upper, False), _POS_INF),
|
|
850
|
+
]
|
|
851
|
+
|
|
852
|
+
if op == "~=":
|
|
853
|
+
prefix = version.__replace__(release=version.release[:-1])
|
|
854
|
+
return [
|
|
855
|
+
(_LowerBound(version, True), _UpperBound(_next_prefix_dev0(prefix), False))
|
|
856
|
+
]
|
|
857
|
+
|
|
858
|
+
raise ValueError(f"Unknown operator: {op!r}") # pragma: no cover
|
|
859
|
+
|
|
860
|
+
|
|
861
|
+
def _format_lower(bound: _LowerBound) -> str:
|
|
862
|
+
if bound.version is None:
|
|
863
|
+
return "(-inf"
|
|
864
|
+
bracket = "[" if bound.inclusive else "("
|
|
865
|
+
inner = (
|
|
866
|
+
bound.version.version
|
|
867
|
+
if isinstance(bound.version, _BoundaryVersion)
|
|
868
|
+
else bound.version
|
|
869
|
+
)
|
|
870
|
+
return f"{bracket}{inner}"
|
|
871
|
+
|
|
872
|
+
|
|
873
|
+
def _format_upper(bound: _UpperBound) -> str:
|
|
874
|
+
if bound.version is None:
|
|
875
|
+
return "+inf)"
|
|
876
|
+
bracket = "]" if bound.inclusive else ")"
|
|
877
|
+
inner = (
|
|
878
|
+
bound.version.version
|
|
879
|
+
if isinstance(bound.version, _BoundaryVersion)
|
|
880
|
+
else bound.version
|
|
881
|
+
)
|
|
882
|
+
return f"{inner}{bracket}"
|
|
883
|
+
|
|
884
|
+
|
|
885
|
+
def _pack_bound(bound: _LowerBound | _UpperBound) -> _PackedBound:
|
|
886
|
+
"""Serialize a bound to a primitive triple. See _PackedBound."""
|
|
887
|
+
bound_version = bound.version
|
|
888
|
+
if bound_version is None:
|
|
889
|
+
return (None, bound.inclusive, None)
|
|
890
|
+
if isinstance(bound_version, _BoundaryVersion):
|
|
891
|
+
return (str(bound_version.version), bound.inclusive, bound_version._kind.name)
|
|
892
|
+
return (str(bound_version), bound.inclusive, None)
|
|
893
|
+
|
|
894
|
+
|
|
895
|
+
def _unpack_bound(
|
|
896
|
+
cls: type[_LowerBound | _UpperBound],
|
|
897
|
+
packed: _PackedBound,
|
|
898
|
+
) -> _LowerBound | _UpperBound:
|
|
899
|
+
"""Reverse of _pack_bound."""
|
|
900
|
+
version_str, inclusive, kind_name = packed
|
|
901
|
+
if version_str is None:
|
|
902
|
+
return cls(None, inclusive)
|
|
903
|
+
base = Version(version_str)
|
|
904
|
+
if kind_name is not None:
|
|
905
|
+
return cls(_BoundaryVersion(base, _BoundaryKind[kind_name]), inclusive)
|
|
906
|
+
return cls(base, inclusive)
|
|
907
|
+
|
|
908
|
+
|
|
909
|
+
def _restore_version_range(
|
|
910
|
+
packed_bounds: tuple[tuple[_PackedBound, _PackedBound], ...],
|
|
911
|
+
arbitrary: str | None = None,
|
|
912
|
+
admit: tuple[str, ...] | None = None,
|
|
913
|
+
reject: tuple[str, ...] | None = None,
|
|
914
|
+
) -> VersionRange:
|
|
915
|
+
"""Pickle restorer; bypasses the ``__new__`` guard via ``_build``.
|
|
916
|
+
|
|
917
|
+
The ``arbitrary`` arg is the pre-admit/reject slot from earlier
|
|
918
|
+
betas. New pickles pass ``admit`` and ``reject`` instead. The
|
|
919
|
+
matched set is preserved either way.
|
|
920
|
+
"""
|
|
921
|
+
bounds = tuple(
|
|
922
|
+
(
|
|
923
|
+
typing.cast("_LowerBound", _unpack_bound(_LowerBound, lower)),
|
|
924
|
+
typing.cast("_UpperBound", _unpack_bound(_UpperBound, upper)),
|
|
925
|
+
)
|
|
926
|
+
for lower, upper in packed_bounds
|
|
927
|
+
)
|
|
928
|
+
if admit is not None or reject is not None:
|
|
929
|
+
return VersionRange._build(
|
|
930
|
+
bounds,
|
|
931
|
+
admit=frozenset(admit or ()),
|
|
932
|
+
reject=frozenset(reject or ()),
|
|
933
|
+
)
|
|
934
|
+
if arbitrary is None:
|
|
935
|
+
return VersionRange._build(bounds)
|
|
936
|
+
# Legacy ``arbitrary`` matched ``{arbitrary}`` if the literal was
|
|
937
|
+
# in bounds, empty otherwise.
|
|
938
|
+
literal_lower = arbitrary.lower()
|
|
939
|
+
legacy_range = VersionRange._build(bounds)
|
|
940
|
+
if literal_lower in legacy_range:
|
|
941
|
+
return VersionRange._build((), admit=frozenset({literal_lower}))
|
|
942
|
+
return VersionRange._build(())
|
|
943
|
+
|
|
944
|
+
|
|
945
|
+
# VersionRange to SpecifierSet conversion is partial: not every range
|
|
946
|
+
# has a SpecifierSet form. Examples that have no single specifier:
|
|
947
|
+
# - PEP 440 ``<V`` excludes pre-releases of V, so the mathematical
|
|
948
|
+
# complement of ``>=V`` (which keeps those pre-releases) has no
|
|
949
|
+
# single specifier.
|
|
950
|
+
# - PEP 440 ``==V`` matches ``V+local`` too, so the strict singleton
|
|
951
|
+
# ``[V, V]`` produced by :meth:`VersionRange.singleton` has none.
|
|
952
|
+
# - Disjoint unions whose gap is not a complete ``==V.*`` family or a
|
|
953
|
+
# ``==V`` family cannot be expressed as ``base & !=...``.
|
|
954
|
+
|
|
955
|
+
|
|
956
|
+
def _is_dev0_version(v: Version) -> bool:
|
|
957
|
+
"""``True`` when *v* is exactly ``X[.Y]*.dev0``: the form ``<X`` produces."""
|
|
958
|
+
return v.dev == 0 and v.pre is None and v.post is None and v.local is None
|
|
959
|
+
|
|
960
|
+
|
|
961
|
+
class _NotEncodable:
|
|
962
|
+
"""Sentinel for "this bound has no PEP 440 specifier representation"."""
|
|
963
|
+
|
|
964
|
+
__slots__ = ()
|
|
965
|
+
|
|
966
|
+
|
|
967
|
+
_NOT_ENCODABLE: Final = _NotEncodable()
|
|
968
|
+
|
|
969
|
+
|
|
970
|
+
def _encode_lower(lower: _LowerBound) -> list[str] | _NotEncodable:
|
|
971
|
+
"""Encode a lower bound as a list of specifier fragments.
|
|
972
|
+
|
|
973
|
+
``[]`` for ``-inf``, one or more fragments otherwise, or
|
|
974
|
+
``_NOT_ENCODABLE`` when the shape has no specifier form.
|
|
975
|
+
AFTER_LOCALS lower bounds emit two fragments (``>=V`` plus
|
|
976
|
+
``!=V``) since the boundary excludes V and every V+local.
|
|
977
|
+
"""
|
|
978
|
+
lower_version = lower.version
|
|
979
|
+
if lower_version is None:
|
|
980
|
+
return []
|
|
981
|
+
if isinstance(lower_version, _BoundaryVersion):
|
|
982
|
+
if lower_version._kind == _BoundaryKind.AFTER_POSTS and not lower.inclusive:
|
|
983
|
+
return [f">{lower_version.version}"]
|
|
984
|
+
if lower_version._kind == _BoundaryKind.AFTER_LOCALS:
|
|
985
|
+
# Strictly above V's local family. ``>=V,!=V`` produces
|
|
986
|
+
# ``[V, +inf)`` minus ``[V, AFTER_LOCALS(V)]``, leaving
|
|
987
|
+
# ``(AFTER_LOCALS(V), +inf)``.
|
|
988
|
+
return [f">={lower_version.version}", f"!={lower_version.version}"]
|
|
989
|
+
# AFTER_POSTS lower with inclusive=True is unreachable from
|
|
990
|
+
# any specifier or set-algebra operation; defensive guard.
|
|
991
|
+
return _NOT_ENCODABLE # pragma: no cover
|
|
992
|
+
if lower.inclusive:
|
|
993
|
+
return [f">={lower_version}"]
|
|
994
|
+
return _NOT_ENCODABLE
|
|
995
|
+
|
|
996
|
+
|
|
997
|
+
def _encode_upper(upper: _UpperBound) -> list[str] | _NotEncodable:
|
|
998
|
+
"""Encode an upper bound as a list of specifier fragments.
|
|
999
|
+
|
|
1000
|
+
``[]`` for ``+inf``, one or more fragments otherwise, or
|
|
1001
|
+
``_NOT_ENCODABLE`` when the shape has no specifier form.
|
|
1002
|
+
"""
|
|
1003
|
+
upper_version = upper.version
|
|
1004
|
+
if upper_version is None:
|
|
1005
|
+
return []
|
|
1006
|
+
if isinstance(upper_version, _BoundaryVersion):
|
|
1007
|
+
if upper_version._kind == _BoundaryKind.AFTER_LOCALS and upper.inclusive:
|
|
1008
|
+
return [f"<={upper_version.version}"]
|
|
1009
|
+
return _NOT_ENCODABLE
|
|
1010
|
+
if not upper.inclusive:
|
|
1011
|
+
if _is_dev0_version(upper_version):
|
|
1012
|
+
# <V produces upper = V.dev0 (excl); strip the synthetic
|
|
1013
|
+
# dev0 to recover the original V.
|
|
1014
|
+
return [f"<{upper_version.__replace__(dev=None)}"]
|
|
1015
|
+
# V (excl) upper: strictly less than V cmpkey-wise, including
|
|
1016
|
+
# V's pre-releases. <=V,!=V produces (-inf, AFTER_LOCALS(V)]
|
|
1017
|
+
# minus [V, AFTER_LOCALS(V)], leaving (-inf, V (excl)).
|
|
1018
|
+
return [f"<={upper_version}", f"!={upper_version}"]
|
|
1019
|
+
return _NOT_ENCODABLE
|
|
1020
|
+
|
|
1021
|
+
|
|
1022
|
+
def _encode_interval(
|
|
1023
|
+
lower: _LowerBound,
|
|
1024
|
+
upper: _UpperBound,
|
|
1025
|
+
) -> list[str] | None:
|
|
1026
|
+
"""Encode one interval as a list of specifier fragments, or ``None``.
|
|
1027
|
+
|
|
1028
|
+
Special-cases ``[V, V]`` (singleton interval) when V carries a
|
|
1029
|
+
local segment: ``==V+local`` matches only that literal, so the
|
|
1030
|
+
interval round-trips. Without a local, no specifier form exists
|
|
1031
|
+
(``==V`` is wider since it also matches ``V+local``).
|
|
1032
|
+
"""
|
|
1033
|
+
if (
|
|
1034
|
+
lower.version is not None
|
|
1035
|
+
and upper.version is not None
|
|
1036
|
+
and not isinstance(lower.version, _BoundaryVersion)
|
|
1037
|
+
and not isinstance(upper.version, _BoundaryVersion)
|
|
1038
|
+
and lower.inclusive
|
|
1039
|
+
and upper.inclusive
|
|
1040
|
+
and lower.version == upper.version
|
|
1041
|
+
and lower.version.local is not None
|
|
1042
|
+
):
|
|
1043
|
+
return [f"=={lower.version}"]
|
|
1044
|
+
lower_parts = _encode_lower(lower)
|
|
1045
|
+
if isinstance(lower_parts, _NotEncodable):
|
|
1046
|
+
return None
|
|
1047
|
+
upper_parts = _encode_upper(upper)
|
|
1048
|
+
if isinstance(upper_parts, _NotEncodable):
|
|
1049
|
+
return None
|
|
1050
|
+
return lower_parts + upper_parts
|
|
1051
|
+
|
|
1052
|
+
|
|
1053
|
+
def _detect_not_equal(
|
|
1054
|
+
left_upper: _UpperBound,
|
|
1055
|
+
right_lower: _LowerBound,
|
|
1056
|
+
) -> Version | None:
|
|
1057
|
+
"""If ``[..., V (excl)] [AFTER_LOCALS(V) (excl), ...]`` matches, return V.
|
|
1058
|
+
|
|
1059
|
+
The gap shape ``!=V`` produces when intersected with surrounding
|
|
1060
|
+
bounds. Only ``!=V`` pattern that can appear inside a multi-interval
|
|
1061
|
+
range.
|
|
1062
|
+
"""
|
|
1063
|
+
if isinstance(left_upper.version, _BoundaryVersion):
|
|
1064
|
+
return None
|
|
1065
|
+
if left_upper.version is None or left_upper.inclusive:
|
|
1066
|
+
return None
|
|
1067
|
+
if not isinstance(right_lower.version, _BoundaryVersion):
|
|
1068
|
+
return None
|
|
1069
|
+
if right_lower.version._kind != _BoundaryKind.AFTER_LOCALS:
|
|
1070
|
+
return None
|
|
1071
|
+
if right_lower.inclusive:
|
|
1072
|
+
# AFTER_LOCALS lower with inclusive=True does not arise from
|
|
1073
|
+
# any specifier or set-algebra operation; defensive guard.
|
|
1074
|
+
return None # pragma: no cover
|
|
1075
|
+
if right_lower.version.version != left_upper.version:
|
|
1076
|
+
# The ``!=V`` pattern is contiguous; mismatched V means a union
|
|
1077
|
+
# of unrelated ranges. Defensive.
|
|
1078
|
+
return None # pragma: no cover
|
|
1079
|
+
return left_upper.version
|
|
1080
|
+
|
|
1081
|
+
|
|
1082
|
+
def _detect_not_equal_wildcard(
|
|
1083
|
+
left_upper: _UpperBound,
|
|
1084
|
+
right_lower: _LowerBound,
|
|
1085
|
+
) -> Version | None:
|
|
1086
|
+
"""If ``[..., V.dev0 (excl)] [V_next.dev0 (incl), ...]`` matches, return V.
|
|
1087
|
+
|
|
1088
|
+
The gap shape ``!=V.*`` produces. ``V`` and ``V_next`` share an
|
|
1089
|
+
epoch and a release prefix differing only in the final component
|
|
1090
|
+
being incremented by one. Returns the prefix version (without the
|
|
1091
|
+
synthetic ``.dev0``) so the caller can write ``!=V.*``.
|
|
1092
|
+
"""
|
|
1093
|
+
left_upper_v = left_upper.version
|
|
1094
|
+
right_lower_v = right_lower.version
|
|
1095
|
+
if isinstance(left_upper_v, _BoundaryVersion) or isinstance(
|
|
1096
|
+
right_lower_v, _BoundaryVersion
|
|
1097
|
+
):
|
|
1098
|
+
return None
|
|
1099
|
+
if left_upper_v is None or right_lower_v is None:
|
|
1100
|
+
# First-interval upper or last-interval lower at infinity means
|
|
1101
|
+
# the interval is the universe and no second interval exists.
|
|
1102
|
+
return None # pragma: no cover
|
|
1103
|
+
if left_upper.inclusive or not right_lower.inclusive:
|
|
1104
|
+
return None
|
|
1105
|
+
if not (_is_dev0_version(left_upper_v) and _is_dev0_version(right_lower_v)):
|
|
1106
|
+
return None
|
|
1107
|
+
if left_upper_v.epoch != right_lower_v.epoch:
|
|
1108
|
+
return None
|
|
1109
|
+
left_release = left_upper_v.release
|
|
1110
|
+
right_release = right_lower_v.release
|
|
1111
|
+
if len(left_release) != len(right_release) or not left_release:
|
|
1112
|
+
return None
|
|
1113
|
+
# All components except the last must match; the last increments by 1.
|
|
1114
|
+
if left_release[:-1] != right_release[:-1]:
|
|
1115
|
+
return None
|
|
1116
|
+
if right_release[-1] != left_release[-1] + 1:
|
|
1117
|
+
return None
|
|
1118
|
+
return left_upper_v.__replace__(dev=None)
|
|
1119
|
+
|
|
1120
|
+
|
|
1121
|
+
class VersionRange:
|
|
1122
|
+
"""A set of :class:`~packaging.version.Version` values, expressed as
|
|
1123
|
+
a union of disjoint intervals on the PEP 440 version ordering.
|
|
1124
|
+
|
|
1125
|
+
Construct with :meth:`from_specifier` / :meth:`from_specifier_set`,
|
|
1126
|
+
or via :meth:`Specifier.to_range` / :meth:`SpecifierSet.to_range`.
|
|
1127
|
+
Compose with :meth:`intersection`, :meth:`union`, :meth:`complement`
|
|
1128
|
+
(and the ``&`` / ``|`` / ``~`` operator aliases).
|
|
1129
|
+
|
|
1130
|
+
>>> r = VersionRange.from_specifier_set(SpecifierSet(">=1.0,<2.0"))
|
|
1131
|
+
>>> "1.5" in r
|
|
1132
|
+
True
|
|
1133
|
+
>>> "2.0" in r
|
|
1134
|
+
False
|
|
1135
|
+
>>> bool(VersionRange.from_specifier_set(SpecifierSet(">=2.0,<1.0")))
|
|
1136
|
+
False
|
|
1137
|
+
|
|
1138
|
+
PEP 440's ``===`` operator matches a candidate string verbatim
|
|
1139
|
+
(case-insensitive) rather than a set of :class:`Version` values.
|
|
1140
|
+
Ranges built from ``===`` specifiers still support membership,
|
|
1141
|
+
set operations, and conversion back to a :class:`SpecifierSet`;
|
|
1142
|
+
matching follows the literal-equality rule instead of the
|
|
1143
|
+
version-ordering rule.
|
|
1144
|
+
"""
|
|
1145
|
+
|
|
1146
|
+
__slots__ = ("_admit", "_bounds", "_reject")
|
|
1147
|
+
_bounds: tuple[_VersionRange, ...]
|
|
1148
|
+
#: Case-folded strings the range admits in addition to its bounds.
|
|
1149
|
+
#: ``===wat`` produces ``_admit = {"wat"}``.
|
|
1150
|
+
_admit: frozenset[str]
|
|
1151
|
+
#: Case-folded strings the range rejects. Overrides ``_admit`` and
|
|
1152
|
+
#: ``_bounds``. Populated by :meth:`complement` of a range whose
|
|
1153
|
+
#: ``_admit`` was non-empty.
|
|
1154
|
+
_reject: frozenset[str]
|
|
1155
|
+
|
|
1156
|
+
def __new__(cls, *args: object, **kwargs: object) -> VersionRange: # noqa: PYI034
|
|
1157
|
+
raise TypeError(
|
|
1158
|
+
"cannot create 'VersionRange' instances directly; use "
|
|
1159
|
+
"VersionRange.from_specifier(), "
|
|
1160
|
+
"VersionRange.from_specifier_set(), "
|
|
1161
|
+
"Specifier.to_range(), or SpecifierSet.to_range() instead"
|
|
1162
|
+
)
|
|
1163
|
+
|
|
1164
|
+
@classmethod
|
|
1165
|
+
def _build(
|
|
1166
|
+
cls,
|
|
1167
|
+
bounds: tuple[_VersionRange, ...],
|
|
1168
|
+
admit: frozenset[str] = frozenset(),
|
|
1169
|
+
reject: frozenset[str] = frozenset(),
|
|
1170
|
+
) -> VersionRange:
|
|
1171
|
+
"""Internal factory; bypasses :meth:`__new__`.
|
|
1172
|
+
|
|
1173
|
+
Drops admit literals already covered by bounds and reject
|
|
1174
|
+
literals already outside bounds. Reject wins over admit on
|
|
1175
|
+
overlap.
|
|
1176
|
+
"""
|
|
1177
|
+
if admit and reject:
|
|
1178
|
+
admit = admit - reject
|
|
1179
|
+
if admit:
|
|
1180
|
+
admit = frozenset(s for s in admit if not _bound_match_string(bounds, s))
|
|
1181
|
+
if reject:
|
|
1182
|
+
reject = frozenset(s for s in reject if _bound_match_string(bounds, s))
|
|
1183
|
+
instance = object.__new__(cls)
|
|
1184
|
+
instance._bounds = bounds
|
|
1185
|
+
instance._admit = admit
|
|
1186
|
+
instance._reject = reject
|
|
1187
|
+
return instance
|
|
1188
|
+
|
|
1189
|
+
def _has_literals(self) -> bool:
|
|
1190
|
+
"""``True`` when ``_admit`` or ``_reject`` is non-empty."""
|
|
1191
|
+
return bool(self._admit) or bool(self._reject)
|
|
1192
|
+
|
|
1193
|
+
@classmethod
|
|
1194
|
+
def empty(cls) -> VersionRange:
|
|
1195
|
+
"""Return the empty range. No version satisfies it.
|
|
1196
|
+
|
|
1197
|
+
>>> VersionRange.empty().is_empty
|
|
1198
|
+
True
|
|
1199
|
+
>>> "1.0" in VersionRange.empty()
|
|
1200
|
+
False
|
|
1201
|
+
"""
|
|
1202
|
+
return cls._build(())
|
|
1203
|
+
|
|
1204
|
+
@classmethod
|
|
1205
|
+
def full(cls) -> VersionRange:
|
|
1206
|
+
"""Return the full range. Every PEP 440 version satisfies it.
|
|
1207
|
+
|
|
1208
|
+
>>> "1.0" in VersionRange.full()
|
|
1209
|
+
True
|
|
1210
|
+
>>> VersionRange.full().is_empty
|
|
1211
|
+
False
|
|
1212
|
+
"""
|
|
1213
|
+
return cls._build(_FULL_RANGE)
|
|
1214
|
+
|
|
1215
|
+
@classmethod
|
|
1216
|
+
def singleton(cls, version: Version | str) -> VersionRange:
|
|
1217
|
+
"""Return the range that contains only *version*.
|
|
1218
|
+
|
|
1219
|
+
>>> r = VersionRange.singleton("1.2.3")
|
|
1220
|
+
>>> "1.2.3" in r
|
|
1221
|
+
True
|
|
1222
|
+
>>> "1.2.4" in r
|
|
1223
|
+
False
|
|
1224
|
+
|
|
1225
|
+
:raises packaging.version.InvalidVersion: if *version* is a
|
|
1226
|
+
string that does not parse as a PEP 440 version.
|
|
1227
|
+
"""
|
|
1228
|
+
if not isinstance(version, Version):
|
|
1229
|
+
version = Version(version)
|
|
1230
|
+
lower = _LowerBound(version, True)
|
|
1231
|
+
upper = _UpperBound(version, True)
|
|
1232
|
+
return cls._build(((lower, upper),))
|
|
1233
|
+
|
|
1234
|
+
def intersection(self, other: VersionRange) -> VersionRange:
|
|
1235
|
+
"""Range containing exactly the versions in both *self* and *other*.
|
|
1236
|
+
|
|
1237
|
+
>>> a = VersionRange.from_specifier_set(SpecifierSet(">=1.0"))
|
|
1238
|
+
>>> b = VersionRange.from_specifier_set(SpecifierSet("<2.0"))
|
|
1239
|
+
>>> ab = VersionRange.from_specifier_set(SpecifierSet(">=1.0,<2.0"))
|
|
1240
|
+
>>> a.intersection(b) == ab
|
|
1241
|
+
True
|
|
1242
|
+
"""
|
|
1243
|
+
if not self._has_literals() and not other._has_literals():
|
|
1244
|
+
return self._build(tuple(_intersect_ranges(self._bounds, other._bounds)))
|
|
1245
|
+
new_bounds = tuple(_intersect_ranges(self._bounds, other._bounds))
|
|
1246
|
+
return self._combine_literals(other, new_bounds, intersect=True)
|
|
1247
|
+
|
|
1248
|
+
def union(self, other: VersionRange) -> VersionRange:
|
|
1249
|
+
"""Range containing every version in *self* or *other*.
|
|
1250
|
+
|
|
1251
|
+
>>> a = VersionRange.singleton("1.0")
|
|
1252
|
+
>>> b = VersionRange.singleton("2.0")
|
|
1253
|
+
>>> "1.0" in a.union(b) and "2.0" in a.union(b)
|
|
1254
|
+
True
|
|
1255
|
+
>>> "1.5" in a.union(b)
|
|
1256
|
+
False
|
|
1257
|
+
"""
|
|
1258
|
+
if not self._has_literals() and not other._has_literals():
|
|
1259
|
+
return self._build(tuple(_union_ranges(self._bounds, other._bounds)))
|
|
1260
|
+
new_bounds = tuple(_union_ranges(self._bounds, other._bounds))
|
|
1261
|
+
return self._combine_literals(other, new_bounds, intersect=False)
|
|
1262
|
+
|
|
1263
|
+
def complement(self) -> VersionRange:
|
|
1264
|
+
"""Range containing every version *not* in *self*.
|
|
1265
|
+
|
|
1266
|
+
>>> r = VersionRange.from_specifier(Specifier(">=1.0"))
|
|
1267
|
+
>>> "0.5" in r.complement()
|
|
1268
|
+
True
|
|
1269
|
+
>>> "1.5" in r.complement()
|
|
1270
|
+
False
|
|
1271
|
+
>>> r.complement().complement() == r
|
|
1272
|
+
True
|
|
1273
|
+
"""
|
|
1274
|
+
if not self._has_literals():
|
|
1275
|
+
return self._build(tuple(_complement_ranges(self._bounds)))
|
|
1276
|
+
# Swap the admit and reject sets, complement the bounds.
|
|
1277
|
+
# ``_build`` drops anything now redundant against the new bounds.
|
|
1278
|
+
return self._build(
|
|
1279
|
+
tuple(_complement_ranges(self._bounds)),
|
|
1280
|
+
admit=self._reject,
|
|
1281
|
+
reject=self._admit,
|
|
1282
|
+
)
|
|
1283
|
+
|
|
1284
|
+
def _combine_literals(
|
|
1285
|
+
self,
|
|
1286
|
+
other: VersionRange,
|
|
1287
|
+
new_bounds: tuple[_VersionRange, ...],
|
|
1288
|
+
*,
|
|
1289
|
+
intersect: bool,
|
|
1290
|
+
) -> VersionRange:
|
|
1291
|
+
"""Resolve admit/reject for ``self & other`` or ``self | other``.
|
|
1292
|
+
|
|
1293
|
+
The bound-only result is already in *new_bounds*. For each
|
|
1294
|
+
literal seen on either side, decide whether the combined
|
|
1295
|
+
predicate (AND for intersection, OR for union) admits it, then
|
|
1296
|
+
record an explicit admit or reject when the new bounds would
|
|
1297
|
+
give the wrong answer on their own.
|
|
1298
|
+
"""
|
|
1299
|
+
admits: set[str] = set()
|
|
1300
|
+
rejects: set[str] = set()
|
|
1301
|
+
for literal in self._admit | self._reject | other._admit | other._reject:
|
|
1302
|
+
self_in = self._matches_literal(literal)
|
|
1303
|
+
other_in = other._matches_literal(literal)
|
|
1304
|
+
want = (self_in and other_in) if intersect else (self_in or other_in)
|
|
1305
|
+
bound_in = _bound_match_string(new_bounds, literal)
|
|
1306
|
+
if want and not bound_in:
|
|
1307
|
+
admits.add(literal)
|
|
1308
|
+
elif not want and bound_in:
|
|
1309
|
+
rejects.add(literal)
|
|
1310
|
+
return self._build(
|
|
1311
|
+
new_bounds, admit=frozenset(admits), reject=frozenset(rejects)
|
|
1312
|
+
)
|
|
1313
|
+
|
|
1314
|
+
def _matches_literal(self, literal: str) -> bool:
|
|
1315
|
+
"""Whether *literal* (case-folded) matches this range's predicate."""
|
|
1316
|
+
if literal in self._reject:
|
|
1317
|
+
return False
|
|
1318
|
+
if literal in self._admit:
|
|
1319
|
+
return True
|
|
1320
|
+
return _bound_match_string(self._bounds, literal)
|
|
1321
|
+
|
|
1322
|
+
def __and__(self, other: object) -> VersionRange:
|
|
1323
|
+
"""Operator alias for :meth:`intersection`."""
|
|
1324
|
+
if not isinstance(other, VersionRange):
|
|
1325
|
+
return NotImplemented
|
|
1326
|
+
return self.intersection(other)
|
|
1327
|
+
|
|
1328
|
+
def __or__(self, other: object) -> VersionRange:
|
|
1329
|
+
"""Operator alias for :meth:`union`."""
|
|
1330
|
+
if not isinstance(other, VersionRange):
|
|
1331
|
+
return NotImplemented
|
|
1332
|
+
return self.union(other)
|
|
1333
|
+
|
|
1334
|
+
def __invert__(self) -> VersionRange:
|
|
1335
|
+
"""Operator alias for :meth:`complement`."""
|
|
1336
|
+
return self.complement()
|
|
1337
|
+
|
|
1338
|
+
def filter(
|
|
1339
|
+
self,
|
|
1340
|
+
iterable: Iterable[Any],
|
|
1341
|
+
key: Callable[[Any], Version | str] | None = None,
|
|
1342
|
+
prereleases: bool | None = None,
|
|
1343
|
+
) -> Iterator[Any]:
|
|
1344
|
+
"""Yield items from *iterable* whose version falls inside the range.
|
|
1345
|
+
|
|
1346
|
+
With *prereleases* ``None`` the PEP 440 default applies:
|
|
1347
|
+
pre-releases are buffered and only emitted if no final release
|
|
1348
|
+
in *iterable* is in range.
|
|
1349
|
+
|
|
1350
|
+
Filtering matches :class:`SpecifierSet.filter` for the same
|
|
1351
|
+
:class:`Specifier` / :class:`SpecifierSet`, including
|
|
1352
|
+
:class:`SpecifierSet("")`'s admission of unparsable strings
|
|
1353
|
+
and the case-insensitive literal match for ``===``.
|
|
1354
|
+
|
|
1355
|
+
>>> r = VersionRange.from_specifier_set(SpecifierSet(">=1.0,<2.0"))
|
|
1356
|
+
>>> list(r.filter(["0.9", "1.5", "2.0"]))
|
|
1357
|
+
['1.5']
|
|
1358
|
+
"""
|
|
1359
|
+
if self._admit or self._reject:
|
|
1360
|
+
return self._filter_with_admission(iterable, key, prereleases)
|
|
1361
|
+
if self._bounds == _FULL_RANGE:
|
|
1362
|
+
# Full-range carve-out: admit any item, parseable or not,
|
|
1363
|
+
# so behaviour matches ``SpecifierSet("").filter``.
|
|
1364
|
+
return self._filter_with_admission(iterable, key, prereleases)
|
|
1365
|
+
return _filter_by_ranges(self._bounds, iterable, key, prereleases)
|
|
1366
|
+
|
|
1367
|
+
def _filter_with_admission(
|
|
1368
|
+
self,
|
|
1369
|
+
iterable: Iterable[Any],
|
|
1370
|
+
key: Callable[[Any], Version | str] | None,
|
|
1371
|
+
prereleases: bool | None,
|
|
1372
|
+
) -> Iterator[Any]:
|
|
1373
|
+
"""Filter for ranges that admit unparsable strings.
|
|
1374
|
+
|
|
1375
|
+
Used by ``===`` ranges (literal admit/reject) and the full-range
|
|
1376
|
+
carve-out. Same PEP 440 pre-release buffering for both, with a
|
|
1377
|
+
different admission check.
|
|
1378
|
+
"""
|
|
1379
|
+
admit_set = self._admit
|
|
1380
|
+
reject_set = self._reject
|
|
1381
|
+
full_bounds = self._bounds == _FULL_RANGE
|
|
1382
|
+
|
|
1383
|
+
def admit(item: Any) -> tuple[bool, Version | None]: # noqa: ANN401
|
|
1384
|
+
raw: Version | str = item if key is None else key(item)
|
|
1385
|
+
raw_lower = str(raw).lower()
|
|
1386
|
+
if reject_set and raw_lower in reject_set:
|
|
1387
|
+
return False, None
|
|
1388
|
+
if admit_set and raw_lower in admit_set:
|
|
1389
|
+
return True, _coerce_version(raw)
|
|
1390
|
+
parsed = _coerce_version(raw)
|
|
1391
|
+
if parsed is None:
|
|
1392
|
+
return full_bounds, None
|
|
1393
|
+
if not full_bounds and not self._matches_bounds(parsed):
|
|
1394
|
+
return False, None
|
|
1395
|
+
return True, parsed
|
|
1396
|
+
|
|
1397
|
+
if prereleases is True:
|
|
1398
|
+
for item in iterable:
|
|
1399
|
+
ok, _ = admit(item)
|
|
1400
|
+
if ok:
|
|
1401
|
+
yield item
|
|
1402
|
+
return
|
|
1403
|
+
|
|
1404
|
+
if prereleases is False:
|
|
1405
|
+
for item in iterable:
|
|
1406
|
+
ok, parsed = admit(item)
|
|
1407
|
+
if not ok:
|
|
1408
|
+
continue
|
|
1409
|
+
if parsed is not None and parsed.is_prerelease:
|
|
1410
|
+
continue
|
|
1411
|
+
yield item
|
|
1412
|
+
return
|
|
1413
|
+
|
|
1414
|
+
# PEP 440 default: yield finals immediately; buffer the rest
|
|
1415
|
+
# until we know whether any final exists.
|
|
1416
|
+
all_nonfinal: list[Any] = []
|
|
1417
|
+
arbitrary_strings: list[Any] = []
|
|
1418
|
+
found_final = False
|
|
1419
|
+
for item in iterable:
|
|
1420
|
+
ok, parsed = admit(item)
|
|
1421
|
+
if not ok:
|
|
1422
|
+
continue
|
|
1423
|
+
if parsed is None:
|
|
1424
|
+
if found_final:
|
|
1425
|
+
yield item
|
|
1426
|
+
else:
|
|
1427
|
+
arbitrary_strings.append(item)
|
|
1428
|
+
all_nonfinal.append(item)
|
|
1429
|
+
continue
|
|
1430
|
+
if not parsed.is_prerelease:
|
|
1431
|
+
if not found_final:
|
|
1432
|
+
yield from arbitrary_strings
|
|
1433
|
+
arbitrary_strings.clear()
|
|
1434
|
+
found_final = True
|
|
1435
|
+
yield item
|
|
1436
|
+
continue
|
|
1437
|
+
if not found_final:
|
|
1438
|
+
all_nonfinal.append(item)
|
|
1439
|
+
if not found_final:
|
|
1440
|
+
yield from all_nonfinal
|
|
1441
|
+
|
|
1442
|
+
@classmethod
|
|
1443
|
+
def from_specifier(cls, specifier: Specifier) -> VersionRange:
|
|
1444
|
+
"""Return the :class:`VersionRange` accepted by *specifier*.
|
|
1445
|
+
|
|
1446
|
+
Results are cached on the *specifier* instance.
|
|
1447
|
+
|
|
1448
|
+
>>> isinstance(VersionRange.from_specifier(Specifier(">=1.0")), VersionRange)
|
|
1449
|
+
True
|
|
1450
|
+
"""
|
|
1451
|
+
cached = specifier._range_cache
|
|
1452
|
+
if cached is not None:
|
|
1453
|
+
return cached
|
|
1454
|
+
|
|
1455
|
+
op = specifier.operator
|
|
1456
|
+
if op == "===":
|
|
1457
|
+
arb_result = cls._build((), admit=frozenset({specifier.version.lower()}))
|
|
1458
|
+
specifier._range_cache = arb_result
|
|
1459
|
+
return arb_result
|
|
1460
|
+
|
|
1461
|
+
ver_str = specifier.version
|
|
1462
|
+
result: VersionRange
|
|
1463
|
+
if ver_str.endswith(".*"):
|
|
1464
|
+
base = specifier._require_spec_version(ver_str[:-2])
|
|
1465
|
+
result = cls._build(tuple(_wildcard_ranges(op, base)))
|
|
1466
|
+
else:
|
|
1467
|
+
version = specifier._require_spec_version(ver_str)
|
|
1468
|
+
has_local = "+" in ver_str
|
|
1469
|
+
result = cls._build(tuple(_standard_ranges(op, version, has_local)))
|
|
1470
|
+
|
|
1471
|
+
specifier._range_cache = result
|
|
1472
|
+
return result
|
|
1473
|
+
|
|
1474
|
+
@classmethod
|
|
1475
|
+
def from_specifier_set(cls, specifier_set: SpecifierSet) -> VersionRange:
|
|
1476
|
+
"""Return the :class:`VersionRange` accepted by *specifier_set*.
|
|
1477
|
+
|
|
1478
|
+
The intersection of every specifier in the set. An empty
|
|
1479
|
+
:class:`SpecifierSet` yields the unbounded range; an
|
|
1480
|
+
unsatisfiable set yields an empty :class:`VersionRange`.
|
|
1481
|
+
Results are cached on the *specifier_set* instance.
|
|
1482
|
+
|
|
1483
|
+
>>> isinstance(
|
|
1484
|
+
... VersionRange.from_specifier_set(SpecifierSet(">=1.0,<2.0")),
|
|
1485
|
+
... VersionRange,
|
|
1486
|
+
... )
|
|
1487
|
+
True
|
|
1488
|
+
>>> VersionRange.from_specifier_set(SpecifierSet(">=2.0,<1.0")).is_empty
|
|
1489
|
+
True
|
|
1490
|
+
"""
|
|
1491
|
+
cached = specifier_set._range_cache
|
|
1492
|
+
if cached is not None:
|
|
1493
|
+
return cached
|
|
1494
|
+
|
|
1495
|
+
# ``===`` literals are handled separately from rangelike specs:
|
|
1496
|
+
# the rangelike specs build the bounds, and a single literal
|
|
1497
|
+
# is admitted only if it also satisfies those bounds.
|
|
1498
|
+
arbitrary_specs = [s for s in specifier_set._specs if s.operator == "==="]
|
|
1499
|
+
rangelike_specs = [s for s in specifier_set._specs if s.operator != "==="]
|
|
1500
|
+
|
|
1501
|
+
if not rangelike_specs:
|
|
1502
|
+
rangelike_result: VersionRange = cls._build(_FULL_RANGE)
|
|
1503
|
+
else:
|
|
1504
|
+
tmp: VersionRange | None = None
|
|
1505
|
+
for s in rangelike_specs:
|
|
1506
|
+
sub = cls.from_specifier(s)
|
|
1507
|
+
if tmp is None:
|
|
1508
|
+
tmp = sub
|
|
1509
|
+
else:
|
|
1510
|
+
tmp = tmp.intersection(sub)
|
|
1511
|
+
if tmp.is_empty:
|
|
1512
|
+
break
|
|
1513
|
+
assert tmp is not None
|
|
1514
|
+
rangelike_result = tmp
|
|
1515
|
+
|
|
1516
|
+
if not arbitrary_specs:
|
|
1517
|
+
specifier_set._range_cache = rangelike_result
|
|
1518
|
+
return rangelike_result
|
|
1519
|
+
|
|
1520
|
+
# Each ``===L_i`` requires the candidate's string to equal L_i.
|
|
1521
|
+
# Distinct literals can never all match, so the result is empty.
|
|
1522
|
+
literals_lower = {s.version.lower() for s in arbitrary_specs}
|
|
1523
|
+
result: VersionRange
|
|
1524
|
+
if len(literals_lower) > 1:
|
|
1525
|
+
result = cls._build(())
|
|
1526
|
+
else:
|
|
1527
|
+
(literal_lower,) = literals_lower
|
|
1528
|
+
if literal_lower in rangelike_result:
|
|
1529
|
+
result = cls._build((), admit=frozenset({literal_lower}))
|
|
1530
|
+
else:
|
|
1531
|
+
result = cls._build(())
|
|
1532
|
+
|
|
1533
|
+
specifier_set._range_cache = result
|
|
1534
|
+
return result
|
|
1535
|
+
|
|
1536
|
+
def to_specifier_set(self) -> SpecifierSet | None:
|
|
1537
|
+
"""Return a single :class:`SpecifierSet` whose
|
|
1538
|
+
:meth:`from_specifier_set` yields *self*, or ``None`` if no
|
|
1539
|
+
such set exists.
|
|
1540
|
+
|
|
1541
|
+
:class:`SpecifierSet` cannot express every range. PEP 440's
|
|
1542
|
+
operator set has no syntax for the strict singleton ``{V}`` or
|
|
1543
|
+
for the bounds produced by complementing ``>V``; for those
|
|
1544
|
+
ranges the result is ``None``. Use :meth:`to_specifier_sets`
|
|
1545
|
+
when a tuple of specifier sets is acceptable. The empty range
|
|
1546
|
+
maps to ``SpecifierSet("<0")`` (``<0`` excludes ``0.dev0``,
|
|
1547
|
+
the smallest PEP 440 version); the full range maps to
|
|
1548
|
+
``SpecifierSet("")``.
|
|
1549
|
+
|
|
1550
|
+
>>> r = VersionRange.from_specifier_set(SpecifierSet(">=1.0,<2.0"))
|
|
1551
|
+
>>> str(r.to_specifier_set())
|
|
1552
|
+
'<2.0,>=1.0'
|
|
1553
|
+
>>> VersionRange.singleton("1.5").to_specifier_set() is None
|
|
1554
|
+
True
|
|
1555
|
+
"""
|
|
1556
|
+
# Local import avoids the circular .specifiers <-> .ranges load.
|
|
1557
|
+
from .specifiers import SpecifierSet # noqa: PLC0415
|
|
1558
|
+
|
|
1559
|
+
if self._reject:
|
|
1560
|
+
# No PEP 440 operator excludes a literal string while
|
|
1561
|
+
# admitting other versions.
|
|
1562
|
+
return None
|
|
1563
|
+
if self._admit:
|
|
1564
|
+
return self._admit_to_specifier_set()
|
|
1565
|
+
if self.is_empty:
|
|
1566
|
+
# ``<0`` parses to upper = 0.dev0 (excl), the smallest
|
|
1567
|
+
# possible PEP 440 version, so the range contains nothing.
|
|
1568
|
+
return SpecifierSet("<0")
|
|
1569
|
+
if self._bounds == _FULL_RANGE:
|
|
1570
|
+
return SpecifierSet("")
|
|
1571
|
+
|
|
1572
|
+
# Walk left-to-right, merging adjacent intervals whose gap is
|
|
1573
|
+
# a ``!=V`` or ``!=V.*`` exclusion. The merged outer bounds
|
|
1574
|
+
# plus the chain of ``!=`` fragments form a single SpecifierSet.
|
|
1575
|
+
bounds = list(self._bounds)
|
|
1576
|
+
outer_lower = bounds[0][0]
|
|
1577
|
+
outer_upper = bounds[0][1]
|
|
1578
|
+
exclusions: list[str] = []
|
|
1579
|
+
for next_lower, next_upper in bounds[1:]:
|
|
1580
|
+
not_equal = _detect_not_equal(outer_upper, next_lower)
|
|
1581
|
+
not_equal_wildcard = _detect_not_equal_wildcard(outer_upper, next_lower)
|
|
1582
|
+
if not_equal is not None:
|
|
1583
|
+
exclusions.append(f"!={not_equal}")
|
|
1584
|
+
elif not_equal_wildcard is not None:
|
|
1585
|
+
exclusions.append(f"!={not_equal_wildcard}.*")
|
|
1586
|
+
else:
|
|
1587
|
+
return None
|
|
1588
|
+
outer_upper = next_upper
|
|
1589
|
+
|
|
1590
|
+
outer_parts = _encode_interval(outer_lower, outer_upper)
|
|
1591
|
+
if outer_parts is None:
|
|
1592
|
+
return None
|
|
1593
|
+
return SpecifierSet(",".join(outer_parts + exclusions))
|
|
1594
|
+
|
|
1595
|
+
def to_specifier_sets(self) -> tuple[SpecifierSet, ...] | None:
|
|
1596
|
+
"""Return a tuple of :class:`SpecifierSet` whose union equals
|
|
1597
|
+
*self*, or ``None`` if no such tuple exists.
|
|
1598
|
+
|
|
1599
|
+
Looser than :meth:`to_specifier_set`: a range that fits a
|
|
1600
|
+
single :class:`SpecifierSet` returns a one-tuple, otherwise
|
|
1601
|
+
each interval encodes separately. ``None`` only for ranges
|
|
1602
|
+
whose individual intervals still have no PEP 440 specifier
|
|
1603
|
+
(for example the singleton produced by :meth:`singleton`).
|
|
1604
|
+
|
|
1605
|
+
>>> r = (
|
|
1606
|
+
... VersionRange.from_specifier_set(SpecifierSet(">=1.0,<2.0"))
|
|
1607
|
+
... | VersionRange.from_specifier_set(SpecifierSet(">=3.0,<4.0"))
|
|
1608
|
+
... )
|
|
1609
|
+
>>> [str(s) for s in r.to_specifier_sets()]
|
|
1610
|
+
['<2.0,>=1.0', '<4.0,>=3.0']
|
|
1611
|
+
>>> VersionRange.singleton("1.5").to_specifier_sets() is None
|
|
1612
|
+
True
|
|
1613
|
+
"""
|
|
1614
|
+
from .specifiers import SpecifierSet # noqa: PLC0415
|
|
1615
|
+
|
|
1616
|
+
if self._reject:
|
|
1617
|
+
return None
|
|
1618
|
+
if self._admit:
|
|
1619
|
+
single = self._admit_to_specifier_set()
|
|
1620
|
+
if single is None:
|
|
1621
|
+
return None
|
|
1622
|
+
return (single,)
|
|
1623
|
+
if self.is_empty:
|
|
1624
|
+
return (SpecifierSet("<0"),)
|
|
1625
|
+
if self._bounds == _FULL_RANGE:
|
|
1626
|
+
return (SpecifierSet(""),)
|
|
1627
|
+
|
|
1628
|
+
# Prefer the single-set form when it exists; that catches
|
|
1629
|
+
# multi-interval ``!=V`` / ``!=V.*`` patterns the per-interval
|
|
1630
|
+
# encoder rejects.
|
|
1631
|
+
single = self.to_specifier_set()
|
|
1632
|
+
if single is not None:
|
|
1633
|
+
return (single,)
|
|
1634
|
+
|
|
1635
|
+
out: list[SpecifierSet] = []
|
|
1636
|
+
for lower, upper in self._bounds:
|
|
1637
|
+
parts = _encode_interval(lower, upper)
|
|
1638
|
+
if parts is None:
|
|
1639
|
+
return None
|
|
1640
|
+
out.append(SpecifierSet(",".join(parts)))
|
|
1641
|
+
return tuple(out)
|
|
1642
|
+
|
|
1643
|
+
def _admit_to_specifier_set(self) -> SpecifierSet | None:
|
|
1644
|
+
"""Encode a single ``===L`` range as ``SpecifierSet("===L")``.
|
|
1645
|
+
|
|
1646
|
+
Returns ``None`` for shapes PEP 440 cannot express: multiple
|
|
1647
|
+
admit literals (no ``=== A or === B`` syntax), or admit
|
|
1648
|
+
combined with a non-empty bound set.
|
|
1649
|
+
"""
|
|
1650
|
+
from .specifiers import SpecifierSet # noqa: PLC0415
|
|
1651
|
+
|
|
1652
|
+
if len(self._admit) != 1 or self._bounds:
|
|
1653
|
+
return None
|
|
1654
|
+
(literal,) = self._admit
|
|
1655
|
+
return SpecifierSet(f"==={literal}")
|
|
1656
|
+
|
|
1657
|
+
def __reduce__(self) -> tuple[object, ...]:
|
|
1658
|
+
# Pickle to a primitive form (see ``_PackedBound``). The legacy
|
|
1659
|
+
# ``arbitrary`` slot is kept for older restorer signatures.
|
|
1660
|
+
return (
|
|
1661
|
+
_restore_version_range,
|
|
1662
|
+
(
|
|
1663
|
+
tuple(
|
|
1664
|
+
(_pack_bound(lower), _pack_bound(upper))
|
|
1665
|
+
for lower, upper in self._bounds
|
|
1666
|
+
),
|
|
1667
|
+
None,
|
|
1668
|
+
tuple(sorted(self._admit)),
|
|
1669
|
+
tuple(sorted(self._reject)),
|
|
1670
|
+
),
|
|
1671
|
+
)
|
|
1672
|
+
|
|
1673
|
+
@property
|
|
1674
|
+
def is_empty(self) -> bool:
|
|
1675
|
+
"""``True`` if no version or string satisfies this range.
|
|
1676
|
+
|
|
1677
|
+
>>> VersionRange.from_specifier_set(SpecifierSet(">=2,<1")).is_empty
|
|
1678
|
+
True
|
|
1679
|
+
>>> VersionRange.from_specifier_set(SpecifierSet(">=1,<2")).is_empty
|
|
1680
|
+
False
|
|
1681
|
+
"""
|
|
1682
|
+
return not self._bounds and not self._admit
|
|
1683
|
+
|
|
1684
|
+
@property
|
|
1685
|
+
def is_prerelease_only(self) -> bool:
|
|
1686
|
+
"""``True`` when every match is a PEP 440 pre-release.
|
|
1687
|
+
|
|
1688
|
+
Used by :meth:`SpecifierSet.is_unsatisfiable` to detect sets
|
|
1689
|
+
that admit no candidate under the default ``prereleases=False``
|
|
1690
|
+
reading. Returns ``False`` for the empty range.
|
|
1691
|
+
|
|
1692
|
+
>>> r = VersionRange.from_specifier_set(SpecifierSet(">=1.0a1,<1.0rc1"))
|
|
1693
|
+
>>> r.is_prerelease_only
|
|
1694
|
+
True
|
|
1695
|
+
>>> VersionRange.from_specifier(Specifier(">=1.0")).is_prerelease_only
|
|
1696
|
+
False
|
|
1697
|
+
"""
|
|
1698
|
+
if self.is_empty:
|
|
1699
|
+
return False
|
|
1700
|
+
if self._reject:
|
|
1701
|
+
return False
|
|
1702
|
+
for literal in self._admit:
|
|
1703
|
+
parsed = _coerce_version(literal)
|
|
1704
|
+
if parsed is None or not parsed.is_prerelease:
|
|
1705
|
+
return False
|
|
1706
|
+
if self._bounds:
|
|
1707
|
+
return _ranges_are_prerelease_only(self._bounds)
|
|
1708
|
+
return True
|
|
1709
|
+
|
|
1710
|
+
def __bool__(self) -> bool:
|
|
1711
|
+
"""``False`` when the range is empty, ``True`` otherwise.
|
|
1712
|
+
|
|
1713
|
+
>>> bool(VersionRange.from_specifier_set(SpecifierSet(">=1,<2")))
|
|
1714
|
+
True
|
|
1715
|
+
>>> bool(VersionRange.from_specifier_set(SpecifierSet(">=2,<1")))
|
|
1716
|
+
False
|
|
1717
|
+
"""
|
|
1718
|
+
return bool(self._bounds) or bool(self._admit)
|
|
1719
|
+
|
|
1720
|
+
def __contains__(self, item: Version | str) -> bool:
|
|
1721
|
+
"""Return whether *item* is contained in this range.
|
|
1722
|
+
|
|
1723
|
+
Unparsable strings do not match, except where
|
|
1724
|
+
:class:`SpecifierSet` would also match: the full range admits
|
|
1725
|
+
any string, and a ``===`` range admits items whose string
|
|
1726
|
+
equals the literal case-insensitively.
|
|
1727
|
+
|
|
1728
|
+
>>> r = VersionRange.from_specifier_set(SpecifierSet(">=1.0,<2.0"))
|
|
1729
|
+
>>> "1.5" in r
|
|
1730
|
+
True
|
|
1731
|
+
>>> "2.0" in r
|
|
1732
|
+
False
|
|
1733
|
+
"""
|
|
1734
|
+
if self._admit or self._reject:
|
|
1735
|
+
item_str = str(item).lower()
|
|
1736
|
+
if item_str in self._reject:
|
|
1737
|
+
return False
|
|
1738
|
+
if item_str in self._admit:
|
|
1739
|
+
return True
|
|
1740
|
+
if self._bounds == _FULL_RANGE:
|
|
1741
|
+
# ``SpecifierSet("")`` admits any string. Match that.
|
|
1742
|
+
return True
|
|
1743
|
+
if not isinstance(item, Version):
|
|
1744
|
+
try:
|
|
1745
|
+
item = Version(item)
|
|
1746
|
+
except InvalidVersion:
|
|
1747
|
+
return False
|
|
1748
|
+
return self._matches_bounds(item)
|
|
1749
|
+
|
|
1750
|
+
def _matches_bounds(self, item: Version) -> bool:
|
|
1751
|
+
"""Bound-only membership check; ignores admit/reject."""
|
|
1752
|
+
return _matches_bounds_only(self._bounds, item)
|
|
1753
|
+
|
|
1754
|
+
def __eq__(self, other: object) -> bool:
|
|
1755
|
+
"""Structural equality. Two ranges are equal when they admit
|
|
1756
|
+
exactly the same set of versions and strings.
|
|
1757
|
+
|
|
1758
|
+
>>> a = VersionRange.from_specifier_set(SpecifierSet(">=1.0,<2.0"))
|
|
1759
|
+
>>> b = VersionRange.from_specifier_set(SpecifierSet(">=1.0,<2.0"))
|
|
1760
|
+
>>> a == b
|
|
1761
|
+
True
|
|
1762
|
+
"""
|
|
1763
|
+
if not isinstance(other, VersionRange):
|
|
1764
|
+
return NotImplemented
|
|
1765
|
+
return (
|
|
1766
|
+
self._bounds == other._bounds
|
|
1767
|
+
and self._admit == other._admit
|
|
1768
|
+
and self._reject == other._reject
|
|
1769
|
+
)
|
|
1770
|
+
|
|
1771
|
+
def __hash__(self) -> int:
|
|
1772
|
+
if not self._admit and not self._reject:
|
|
1773
|
+
return hash(self._bounds)
|
|
1774
|
+
return hash((self._bounds, self._admit, self._reject))
|
|
1775
|
+
|
|
1776
|
+
def __repr__(self) -> str:
|
|
1777
|
+
"""Human-readable representation. Internal layout, debugging only.
|
|
1778
|
+
|
|
1779
|
+
>>> VersionRange.from_specifier_set(SpecifierSet(">=1.0,<2.0"))
|
|
1780
|
+
<VersionRange '[1.0, 2.0.dev0)'>
|
|
1781
|
+
>>> VersionRange.from_specifier_set(SpecifierSet(""))
|
|
1782
|
+
<VersionRange '(-inf, +inf)'>
|
|
1783
|
+
>>> VersionRange.from_specifier_set(SpecifierSet(">=2.0,<1.0"))
|
|
1784
|
+
<VersionRange '(empty)'>
|
|
1785
|
+
>>> VersionRange.from_specifier(Specifier("===wat"))
|
|
1786
|
+
<VersionRange '{wat}'>
|
|
1787
|
+
"""
|
|
1788
|
+
if self._bounds:
|
|
1789
|
+
bound_body = " | ".join(
|
|
1790
|
+
f"{_format_lower(lower)}, {_format_upper(upper)}"
|
|
1791
|
+
for lower, upper in self._bounds
|
|
1792
|
+
)
|
|
1793
|
+
else:
|
|
1794
|
+
bound_body = "(empty)" if not self._admit else ""
|
|
1795
|
+
parts: list[str] = []
|
|
1796
|
+
if bound_body:
|
|
1797
|
+
parts.append(bound_body)
|
|
1798
|
+
if self._admit:
|
|
1799
|
+
parts.append("{" + ", ".join(sorted(self._admit)) + "}")
|
|
1800
|
+
body = " | ".join(parts) if parts else "(empty)"
|
|
1801
|
+
if self._reject:
|
|
1802
|
+
body = f"{body} \\ {{{', '.join(sorted(self._reject))}}}"
|
|
1803
|
+
return f"<{self.__class__.__name__} {body!r}>"
|