Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

pypa packaging compliance #402

Merged
merged 4 commits into from
May 8, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/poetry/core/constraints/version/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

from poetry.core.constraints.version.empty_constraint import EmptyConstraint
from poetry.core.constraints.version.parser import parse_constraint
from poetry.core.constraints.version.parser import parse_marker_version_constraint
from poetry.core.constraints.version.util import constraint_regions
from poetry.core.constraints.version.version import Version
from poetry.core.constraints.version.version_constraint import VersionConstraint
Expand All @@ -21,4 +22,5 @@
"VersionUnion",
"constraint_regions",
"parse_constraint",
"parse_marker_version_constraint",
)
92 changes: 72 additions & 20 deletions src/poetry/core/constraints/version/parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,22 @@


if TYPE_CHECKING:
from poetry.core.constraints.version.version import Version
from poetry.core.constraints.version.version_constraint import VersionConstraint


@functools.lru_cache(maxsize=None)
def parse_constraint(constraints: str) -> VersionConstraint:
return _parse_constraint(constraints=constraints)


def parse_marker_version_constraint(constraints: str) -> VersionConstraint:
return _parse_constraint(constraints=constraints, is_marker_constraint=True)


def _parse_constraint(
constraints: str, *, is_marker_constraint: bool = False
) -> VersionConstraint:
if constraints == "*":
from poetry.core.constraints.version.version_range import VersionRange

Expand All @@ -33,9 +44,17 @@ def parse_constraint(constraints: str) -> VersionConstraint:

if len(and_constraints) > 1:
for constraint in and_constraints:
constraint_objects.append(parse_single_constraint(constraint))
constraint_objects.append(
parse_single_constraint(
constraint, is_marker_constraint=is_marker_constraint
)
)
else:
constraint_objects.append(parse_single_constraint(and_constraints[0]))
constraint_objects.append(
parse_single_constraint(
and_constraints[0], is_marker_constraint=is_marker_constraint
)
)

if len(constraint_objects) == 1:
constraint = constraint_objects[0]
Expand All @@ -54,7 +73,9 @@ def parse_constraint(constraints: str) -> VersionConstraint:
return VersionUnion.of(*or_groups)


def parse_single_constraint(constraint: str) -> VersionConstraint:
def parse_single_constraint(
constraint: str, *, is_marker_constraint: bool = False
) -> VersionConstraint:
from poetry.core.constraints.version.patterns import BASIC_CONSTRAINT
from poetry.core.constraints.version.patterns import CARET_CONSTRAINT
from poetry.core.constraints.version.patterns import TILDE_CONSTRAINT
Expand Down Expand Up @@ -117,25 +138,15 @@ def parse_single_constraint(constraint: str) -> VersionConstraint:
m = X_CONSTRAINT.match(constraint)
if m:
op = m.group("op")
major = int(m.group(2))
minor = m.group(3)

if minor is not None:
version = Version.from_parts(major, int(minor), 0)
result: VersionConstraint = VersionRange(
version, version.next_minor(), include_min=True
try:
return _make_x_constraint_range(
version=Version.parse(m.group("version")),
invert=op == "!=",
is_marker_constraint=is_marker_constraint,
)
elif major == 0:
result = VersionRange(max=Version.from_parts(1, 0, 0))
else:
version = Version.from_parts(major, 0, 0)

result = VersionRange(version, version.next_major(), include_min=True)

if op == "!=":
result = VersionRange().difference(result)

return result
except ValueError:
raise ValueError(f"Could not parse version constraint: {constraint}")

# Basic comparator
m = BASIC_CONSTRAINT.match(constraint)
Expand All @@ -161,8 +172,49 @@ def parse_single_constraint(constraint: str) -> VersionConstraint:
return VersionRange(min=version)
if op == ">=":
return VersionRange(min=version, include_min=True)

if m.group("wildcard") is not None:
return _make_x_constraint_range(
version=version,
invert=op == "!=",
is_marker_constraint=is_marker_constraint,
)

if op == "!=":
return VersionUnion(VersionRange(max=version), VersionRange(min=version))

return version

raise ParseConstraintError(f"Could not parse version constraint: {constraint}")


def _make_x_constraint_range(
version: Version, *, invert: bool = False, is_marker_constraint: bool = False
) -> VersionConstraint:
from poetry.core.constraints.version.version_range import VersionRange

if version.is_postrelease():
_next = version.next_postrelease()
elif version.is_stable():
_next = version.next_stable()
elif version.is_prerelease():
_next = version.next_prerelease()
elif version.is_devrelease():
_next = version.next_devrelease()
else:
raise RuntimeError("version is neither stable, nor pre-release nor dev-release")

_min = version
_max = _next

if not is_marker_constraint:
_min = _min.first_devrelease()
if not _max.is_devrelease():
_max = _max.first_devrelease()

result = VersionRange(_min, _max, include_min=True)

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

return result
4 changes: 2 additions & 2 deletions src/poetry/core/constraints/version/patterns.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,12 @@
rf"^~=\s*(?P<version>{VERSION_PATTERN})$", re.VERBOSE | re.IGNORECASE
)
X_CONSTRAINT = re.compile(
r"^(?P<op>!=|==)?\s*v?(\d+)(?:\.(\d+))?(?:\.(\d+))?(?:\.[xX*])+$"
r"^(?P<op>!=|==)?\s*v?(?P<version>(\d+)(?:\.(\d+))?(?:\.(\d+))?)(?:\.[xX*])+$"
)

# note that we also allow technically incorrect version patterns with astrix (eg: 3.5.*)
# as this is supported by pip and appears in metadata within python packages
BASIC_CONSTRAINT = re.compile(
rf"^(?P<op><>|!=|>=?|<=?|==?)?\s*(?P<version>{VERSION_PATTERN}|dev)(\.\*)?$",
rf"^(?P<op><>|!=|>=?|<=?|==?)?\s*(?P<version>{VERSION_PATTERN}|dev)(?P<wildcard>\.\*)?$",
re.VERBOSE | re.IGNORECASE,
)
61 changes: 61 additions & 0 deletions src/poetry/core/constraints/version/version_constraint.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,3 +63,64 @@ def __hash__(self) -> int:

def __eq__(self, other: object) -> bool:
raise NotImplementedError


def _is_wildcard_candidate(
min_: Version, max_: Version, *, inverted: bool = False
) -> bool:
if (
min_.is_local()
or max_.is_local()
or min_.is_prerelease()
or max_.is_prerelease()
or min_.is_postrelease() is not max_.is_postrelease()
or min_.first_devrelease() != min_
or (max_.is_devrelease() and max_.first_devrelease() != max_)
):
return False

first = max_ if inverted else min_
second = min_ if inverted else max_

parts_first = list(first.parts)
parts_second = list(second.parts)

# remove trailing zeros from second
while parts_second and parts_second[-1] == 0:
del parts_second[-1]

# fill up first with zeros
parts_first += [0] * (len(parts_second) - len(parts_first))

# all exceeding parts of first must be zero
if set(parts_first[len(parts_second) :]) not in [set(), {0}]:
return False

parts_first = parts_first[: len(parts_second)]

if first.is_postrelease():
assert first.post is not None
return parts_first == parts_second and first.post.next() == second.post

return (
parts_first[:-1] == parts_second[:-1]
and parts_first[-1] + 1 == parts_second[-1]
)


def _single_wildcard_range_string(first: Version, second: Version) -> str:
if first.is_postrelease():
base_version = str(first.without_devrelease())

else:
parts = list(second.parts)

# remove trailing zeros from max
while parts and parts[-1] == 0:
del parts[-1]

parts[-1] = parts[-1] - 1

base_version = ".".join(str(part) for part in parts)

return f"{base_version}.*"
84 changes: 57 additions & 27 deletions src/poetry/core/constraints/version/version_range.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,13 @@
from __future__ import annotations

from contextlib import suppress
from typing import TYPE_CHECKING

from poetry.core.constraints.version.empty_constraint import EmptyConstraint
from poetry.core.constraints.version.version_constraint import _is_wildcard_candidate
from poetry.core.constraints.version.version_constraint import (
_single_wildcard_range_string,
)
from poetry.core.constraints.version.version_range_constraint import (
VersionRangeConstraint,
)
Expand All @@ -21,22 +26,9 @@ def __init__(
max: Version | None = None,
include_min: bool = False,
include_max: bool = False,
always_include_max_prerelease: bool = False,
) -> None:
full_max = max
if (
not always_include_max_prerelease
and not include_max
and full_max is not None
and full_max.is_stable()
and not full_max.is_postrelease()
and (min is None or min.is_stable() or min.release != full_max.release)
):
full_max = full_max.first_prerelease()

self._min = min
self._max = max
self._full_max = full_max
self._min = min
self._include_min = include_min
self._include_max = include_max

Expand All @@ -48,10 +40,6 @@ def min(self) -> Version | None:
def max(self) -> Version | None:
return self._max

@property
def full_max(self) -> Version | None:
return self._full_max

@property
def include_min(self) -> bool:
return self._include_min
Expand All @@ -71,27 +59,43 @@ def is_simple(self) -> bool:

def allows(self, other: Version) -> bool:
if self._min is not None:
if other < self._min:
_this, _other = self.allowed_min, other

assert _this is not None

if not _this.is_postrelease() and _other.is_postrelease():
# The exclusive ordered comparison >V MUST NOT allow a post-release
# of the given version unless V itself is a post release.
# https://peps.python.org/pep-0440/#exclusive-ordered-comparison
# e.g. "2.0.post1" does not match ">2"
_other = _other.without_postrelease()

if not _this.is_local() and _other.is_local():
# The exclusive ordered comparison >V MUST NOT match
# a local version of the specified version.
# https://peps.python.org/pep-0440/#exclusive-ordered-comparison
# e.g. "2.0+local.version" does not match ">2"
_other = other.without_local()

if _other < _this:
return False

if not self._include_min and other == self._min:
if not self._include_min and (_other == self._min or _other == _this):
return False

if self.full_max is not None:
_this, _other = self.full_max, other
if self.max is not None:
_this, _other = self.allowed_max, other

assert _this is not None

if not _this.is_local() and _other.is_local():
# allow weak equality to allow `3.0.0+local.1` for `<=3.0.0`
_other = _other.without_local()

if not _this.is_postrelease() and _other.is_postrelease():
# allow weak equality to allow `3.0.0-1` for `<=3.0.0`
_other = _other.without_postrelease()

if _other > _this:
return False

if not self._include_max and _other == _this:
if not self._include_max and (_other == self._max or _other == _this):
return False

return True
Expand Down Expand Up @@ -335,6 +339,29 @@ def difference(self, other: VersionConstraint) -> VersionConstraint:
def flatten(self) -> list[VersionRangeConstraint]:
return [self]

def _single_wildcard_range_string(self) -> str:
if not self.is_single_wildcard_range():
raise ValueError("Not a valid wildcard range")

assert self.min is not None
assert self.max is not None
return f"=={_single_wildcard_range_string(self.min, self.max)}"

def is_single_wildcard_range(self) -> bool:
# e.g.
# - "1.*" equals ">=1.0.dev0, <2" (equivalent to ">=1.0.dev0, <2.0.dev0")
# - "1.0.*" equals ">=1.0.dev0, <1.1"
# - "1.2.*" equals ">=1.2.dev0, <1.3"
if (
self.min is None
or self.max is None
or not self.include_min
or self.include_max
):
return False

return _is_wildcard_candidate(self.min, self.max)

def __eq__(self, other: object) -> bool:
if not isinstance(other, VersionRangeConstraint):
return False
Expand Down Expand Up @@ -391,6 +418,9 @@ def _compare_max(self, other: VersionRangeConstraint) -> int:
return 0

def __str__(self) -> str:
with suppress(ValueError):
return self._single_wildcard_range_string()

text = ""

if self.min is not None:
Expand Down
Loading