Skip to content

Commit 4aa943a

Browse files
committed
fix and simplify handling of single wildcard range exclusion (e.g. "!=1.2.*")
1 parent 0dfdee0 commit 4aa943a

File tree

9 files changed

+290
-236
lines changed

9 files changed

+290
-236
lines changed

src/poetry/core/constraints/version/parser.py

+5-2
Original file line numberDiff line numberDiff line change
@@ -205,11 +205,14 @@ def _make_x_constraint_range(
205205
raise RuntimeError("version is neither stable, nor pre-release nor dev-release")
206206

207207
_min = version
208+
_max = _next
208209

209-
if not is_marker_constraint and not _next.is_unstable():
210+
if not is_marker_constraint:
210211
_min = _min.first_devrelease()
212+
if not _max.is_devrelease():
213+
_max = _max.first_devrelease()
211214

212-
result = VersionRange(_min, _next, include_min=True)
215+
result = VersionRange(_min, _max, include_min=True)
213216

214217
if invert:
215218
return VersionRange().difference(result)

src/poetry/core/constraints/version/version_constraint.py

+61
Original file line numberDiff line numberDiff line change
@@ -63,3 +63,64 @@ def __hash__(self) -> int:
6363

6464
def __eq__(self, other: object) -> bool:
6565
raise NotImplementedError
66+
67+
68+
def _is_wildcard_candidate(
69+
min_: Version, max_: Version, *, inverted: bool = False
70+
) -> bool:
71+
if (
72+
min_.is_local()
73+
or max_.is_local()
74+
or min_.is_prerelease()
75+
or max_.is_prerelease()
76+
or min_.is_postrelease() is not max_.is_postrelease()
77+
or min_.first_devrelease() != min_
78+
or (max_.is_devrelease() and max_.first_devrelease() != max_)
79+
):
80+
return False
81+
82+
first = max_ if inverted else min_
83+
second = min_ if inverted else max_
84+
85+
parts_first = list(first.parts)
86+
parts_second = list(second.parts)
87+
88+
# remove trailing zeros from second
89+
while parts_second and parts_second[-1] == 0:
90+
del parts_second[-1]
91+
92+
# fill up first with zeros
93+
parts_first += [0] * (len(parts_second) - len(parts_first))
94+
95+
# all exceeding parts of first must be zero
96+
if set(parts_first[len(parts_second) :]) not in [set(), {0}]:
97+
return False
98+
99+
parts_first = parts_first[: len(parts_second)]
100+
101+
if first.is_postrelease():
102+
assert first.post is not None
103+
return parts_first == parts_second and first.post.next() == second.post
104+
105+
return (
106+
parts_first[:-1] == parts_second[:-1]
107+
and parts_first[-1] + 1 == parts_second[-1]
108+
)
109+
110+
111+
def _single_wildcard_range_string(first: Version, second: Version) -> str:
112+
if first.is_postrelease():
113+
base_version = str(first.without_devrelease())
114+
115+
else:
116+
parts = list(second.parts)
117+
118+
# remove trailing zeros from max
119+
while parts and parts[-1] == 0:
120+
del parts[-1]
121+
122+
parts[-1] = parts[-1] - 1
123+
124+
base_version = ".".join(str(part) for part in parts)
125+
126+
return f"{base_version}.*"

src/poetry/core/constraints/version/version_range.py

+8-39
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,10 @@
44
from typing import TYPE_CHECKING
55

66
from poetry.core.constraints.version.empty_constraint import EmptyConstraint
7+
from poetry.core.constraints.version.version_constraint import _is_wildcard_candidate
8+
from poetry.core.constraints.version.version_constraint import (
9+
_single_wildcard_range_string,
10+
)
711
from poetry.core.constraints.version.version_range_constraint import (
812
VersionRangeConstraint,
913
)
@@ -339,23 +343,9 @@ def _single_wildcard_range_string(self) -> str:
339343
if not self.is_single_wildcard_range():
340344
raise ValueError("Not a valid wildcard range")
341345

342-
if self.min.is_postrelease():
343-
assert self.min is not None
344-
base_version = str(self.min.without_devrelease())
345-
346-
else:
347-
assert self.max is not None
348-
parts = list(self.max.parts)
349-
350-
# remove trailing zeros from max
351-
while parts and parts[-1] == 0:
352-
del parts[-1]
353-
354-
parts[-1] = parts[-1] - 1
355-
356-
base_version = ".".join(str(part) for part in parts)
357-
358-
return f"=={base_version}.*"
346+
assert self.min is not None
347+
assert self.max is not None
348+
return f"=={_single_wildcard_range_string(self.min, self.max)}"
359349

360350
def is_single_wildcard_range(self) -> bool:
361351
# e.g.
@@ -367,31 +357,10 @@ def is_single_wildcard_range(self) -> bool:
367357
or self.max is None
368358
or not self.include_min
369359
or self.include_max
370-
or self.min.is_local()
371-
or self.max.is_local()
372-
or self.max.is_prerelease()
373-
or self.min.is_postrelease() is not self.max.is_postrelease()
374-
or self.min.first_devrelease() != self.min
375-
or (self.max.is_devrelease() and self.max.first_devrelease() != self.max)
376360
):
377361
return False
378362

379-
parts_min = list(self.min.parts)
380-
parts_max = list(self.max.parts)
381-
382-
# remove trailing zeros from max
383-
while parts_max and parts_max[-1] == 0:
384-
del parts_max[-1]
385-
386-
# fill up min with zeros
387-
parts_min += [0] * (len(parts_max) - len(parts_min))
388-
389-
if set(parts_min[len(parts_max) :]) not in [set(), {0}]:
390-
return False
391-
parts_min = parts_min[: len(parts_max)]
392-
if self.min.is_postrelease():
393-
return parts_min == parts_max and self.min.post.next() == self.max.post
394-
return parts_min[:-1] == parts_max[:-1] and parts_min[-1] + 1 == parts_max[-1]
363+
return _is_wildcard_candidate(self.min, self.max)
395364

396365
def __eq__(self, other: object) -> bool:
397366
if not isinstance(other, VersionRangeConstraint):

src/poetry/core/constraints/version/version_union.py

+16-147
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@
77

88
from poetry.core.constraints.version.empty_constraint import EmptyConstraint
99
from poetry.core.constraints.version.version_constraint import VersionConstraint
10+
from poetry.core.constraints.version.version_constraint import _is_wildcard_candidate
11+
from poetry.core.constraints.version.version_constraint import (
12+
_single_wildcard_range_string,
13+
)
1014
from poetry.core.constraints.version.version_range_constraint import (
1115
VersionRangeConstraint,
1216
)
@@ -256,168 +260,33 @@ def _exclude_single_wildcard_range_string(self) -> str:
256260
if not self.excludes_single_wildcard_range():
257261
raise ValueError("Not a valid wildcard range")
258262

259-
# we assume here that since it is a single exclusion range
260-
# that it is one of "< 2.0.0 || >= 2.1.0" or ">= 2.1.0 || < 2.0.0"
261-
# and the one with the max is the first part
262263
idx_order = (0, 1) if self._ranges[0].max else (1, 0)
263-
one = self._ranges[idx_order[0]].max
264-
assert one is not None
265-
two = self._ranges[idx_order[1]].min
266-
assert two is not None
267-
268-
# versions can have both semver and non semver parts
269-
parts_one = [
270-
one.major,
271-
one.minor or 0,
272-
one.patch or 0,
273-
*list(one.non_semver_parts or []),
274-
]
275-
parts_two = [
276-
two.major,
277-
two.minor or 0,
278-
two.patch or 0,
279-
*list(two.non_semver_parts or []),
280-
]
281-
282-
# we assume here that a wildcard range implies that the part following the
283-
# first part that is different in the second range is the wildcard, this means
284-
# that multiple wildcards are not supported right now.
285-
parts = []
286-
287-
for idx, part in enumerate(parts_one):
288-
parts.append(str(part))
289-
if parts_two[idx] != part:
290-
# since this part is different the next one is the wildcard
291-
# for example, "< 2.0.0 || >= 2.1.0" gets us a wildcard range
292-
# 2.0.*
293-
parts.append("*")
294-
break
295-
else:
296-
# we should not ever get here, however it is likely that poorly
297-
# constructed metadata exists
298-
raise ValueError("Not a valid wildcard range")
299-
300-
return f"!={'.'.join(parts)}"
301-
302-
@staticmethod
303-
def _excludes_single_wildcard_range_check_is_valid_range(
304-
one: VersionRangeConstraint, two: VersionRangeConstraint
305-
) -> bool:
306-
"""
307-
Helper method to determine if two versions define a single wildcard range.
308-
309-
In cases where !=2.0.* was parsed by us, the union is of the range
310-
<2.0.0 || >=2.1.0. In user defined ranges, precision might be different.
311-
For example, a union <2.0 || >= 2.1.0 is still !=2.0.*. In order to
312-
handle these cases we make sure that if precisions do not match, extra
313-
checks are performed to validate that the constraint is a valid single
314-
wildcard range.
315-
"""
264+
one = self._ranges[idx_order[0]]
265+
two = self._ranges[idx_order[1]]
316266

317267
assert one.max is not None
318268
assert two.min is not None
319-
320-
_max = one.max
321-
_min = two.min
322-
323-
if _max.is_devrelease() and _max.dev is not None and _max.dev.number == 0:
324-
# handle <2.0.0.dev0 || >= 2.1.0
325-
_max = _max.without_devrelease()
326-
327-
if _min.is_devrelease():
328-
assert _min.dev is not None
329-
330-
if _min.dev.number != 0:
331-
# if both are dev releases, they should both have dev0
332-
return False
333-
_min = _min.without_devrelease()
334-
335-
max_precision = max(_max.precision, _min.precision)
336-
337-
if max_precision <= 3:
338-
# In cases where both versions have a precision less than 3,
339-
# we can make use of the next major/minor/patch versions.
340-
return _min in {
341-
_max.next_major(),
342-
_max.next_minor(),
343-
_max.next_patch(),
344-
}
345-
else:
346-
# When there are non-semver parts in one of the versions, we need to
347-
# ensure we use zero padded version and in addition to next major/minor/
348-
# patch versions, also check each next release for the extra parts.
349-
from_parts = _max.__class__.from_parts
350-
351-
_extras: list[list[int]] = []
352-
_versions: list[Version] = []
353-
354-
for _version in (_max, _min):
355-
_extra = list(_version.non_semver_parts or [])
356-
357-
while len(_extra) < (max_precision - 3):
358-
# pad zeros for extra parts to ensure precisions are equal
359-
_extra.append(0)
360-
361-
# create a new release with unspecified parts padded with zeros
362-
_padded_version: Version = from_parts(
363-
major=_version.major,
364-
minor=_version.minor or 0,
365-
patch=_version.patch or 0,
366-
extra=tuple(_extra),
367-
)
368-
369-
_extras.append(_extra)
370-
_versions.append(_padded_version)
371-
372-
_extra_one = _extras[0]
373-
_padded_version_one = _versions[0]
374-
_padded_version_two = _versions[1]
375-
376-
_check_versions = {
377-
_padded_version_one.next_major(),
378-
_padded_version_one.next_minor(),
379-
_padded_version_one.next_patch(),
380-
}
381-
382-
# for each non-semver (extra) part, bump a version
383-
for idx, val in enumerate(_extra_one):
384-
_extra = [
385-
*_extra_one[: idx - 1],
386-
(val + 1),
387-
*_extra_one[idx + 1 :],
388-
]
389-
_check_versions.add(
390-
from_parts(
391-
_padded_version_one.major,
392-
_padded_version_one.minor,
393-
_padded_version_one.patch,
394-
tuple(_extra),
395-
)
396-
)
397-
398-
return _padded_version_two in _check_versions
269+
return f"!={_single_wildcard_range_string(one.max, two.min)}"
399270

400271
def excludes_single_wildcard_range(self) -> bool:
401-
from poetry.core.constraints.version.version_range import VersionRange
402-
403272
if len(self._ranges) != 2:
404273
return False
405274

406275
idx_order = (0, 1) if self._ranges[0].max else (1, 0)
407276
one = self._ranges[idx_order[0]]
408277
two = self._ranges[idx_order[1]]
409278

410-
is_range_exclusion = (
411-
one.max and not one.include_max and two.min and two.include_min
412-
)
413-
414-
if not is_range_exclusion:
415-
return False
416-
417-
if not self._excludes_single_wildcard_range_check_is_valid_range(one, two):
279+
if (
280+
one.max is None
281+
or one.include_max
282+
or one.min is not None
283+
or two.min is None
284+
or not two.include_min
285+
or two.max is not None
286+
):
418287
return False
419288

420-
return isinstance(VersionRange().difference(self), VersionRange)
289+
return _is_wildcard_candidate(two.min, one.max, inverted=True)
421290

422291
def excludes_single_version(self) -> bool:
423292
from poetry.core.constraints.version.version import Version

0 commit comments

Comments
 (0)