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

Fixing platform_release pep440 #722

Merged
merged 3 commits into from
Jul 30, 2024
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
105 changes: 71 additions & 34 deletions src/poetry/core/constraints/generic/constraint.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

import operator

from typing import Any
from typing import Callable
from typing import ClassVar

Expand All @@ -11,20 +10,44 @@
from poetry.core.constraints.generic.empty_constraint import EmptyConstraint


OperatorType = Callable[[object, object], Any]
OperatorType = Callable[[object, object], bool]


def contains(a: object, b: object, /) -> bool:
return operator.contains(a, b) # type: ignore[arg-type]


def not_contains(a: object, b: object, /) -> bool:
return not contains(a, b)


class Constraint(BaseConstraint):
OP_EQ = operator.eq
OP_NE = operator.ne
OP_IN = contains
OP_NC = not_contains

_trans_op_str: ClassVar[dict[str, OperatorType]] = {
"=": OP_EQ,
"==": OP_EQ,
"!=": OP_NE,
"in": OP_IN,
"not in": OP_NC,
}

_trans_op_int: ClassVar[dict[OperatorType, str]] = {OP_EQ: "==", OP_NE: "!="}
_trans_op_int: ClassVar[dict[OperatorType, str]] = {
OP_EQ: "==",
OP_NE: "!=",
OP_IN: "in",
OP_NC: "not in",
}

_trans_op_inv: ClassVar[dict[str, str]] = {
"!=": "==",
"==": "!=",
"not in": "in",
"in": "not in",
}

def __init__(self, value: str, operator: str = "==") -> None:
if operator == "=":
Expand All @@ -49,14 +72,8 @@ def allows(self, other: BaseConstraint) -> bool:
f' ("other" must be a constraint with operator "=="): {other}'
)

is_equal_op = self._operator == "=="
is_non_equal_op = self._operator == "!="

if is_equal_op:
return self._value == other.value

if is_non_equal_op:
return self._value != other.value
if op := self._trans_op_str.get(self._operator):
return op(other.value, self._value)

return False

Expand All @@ -68,6 +85,15 @@ def allows_all(self, other: BaseConstraint) -> bool:
if other.operator == "==":
return self.allows(other)

if other.operator == "in" and self._operator == "in":
return self.value in other.value

if other.operator == "not in":
if self._operator == "not in":
return other.value in self.value
if self._operator == "!=":
return self.value not in other.value

return self == other

if isinstance(other, MultiConstraint):
Expand All @@ -82,36 +108,36 @@ def allows_any(self, other: BaseConstraint) -> bool:
from poetry.core.constraints.generic import MultiConstraint
from poetry.core.constraints.generic import UnionConstraint

is_equal_op = self._operator == "=="
is_non_equal_op = self._operator == "!="

if is_equal_op:
if self._operator == "==":
return other.allows(self)

if isinstance(other, Constraint):
is_other_equal_op = other.operator == "=="
is_other_non_equal_op = other.operator == "!="

if is_other_equal_op:
if other.operator == "==":
return self.allows(other)

if is_equal_op and is_other_non_equal_op:
if other.operator == "!=" and self._operator == "==":
return self._value != other.value

return is_non_equal_op and is_other_non_equal_op
if other.operator == "not in" and self._operator == "in":
return other.value not in self.value

if other.operator == "in" and self._operator == "not in":
return self.value not in other.value

return True

elif isinstance(other, MultiConstraint):
return is_non_equal_op
return self._operator == "!="

elif isinstance(other, UnionConstraint):
return is_non_equal_op and any(
return self._operator == "!=" and any(
self.allows_any(c) for c in other.constraints
)

return other.is_any()

def invert(self) -> Constraint:
return Constraint(self._value, "!=" if self._operator == "==" else "==")
return Constraint(self._value, self._trans_op_inv[self.operator])

def difference(self, other: BaseConstraint) -> Constraint | EmptyConstraint:
if other.allows(self):
Expand All @@ -126,16 +152,16 @@ def intersect(self, other: BaseConstraint) -> BaseConstraint:
if other == self:
return self

if self.operator == "!=" and other.operator == "==" and self.allows(other):
if self.allows_all(other):
return other

if other.operator == "!=" and self.operator == "==" and other.allows(self):
if other.allows_all(self):
return self

if other.operator == "!=" and self.operator == "!=":
return MultiConstraint(self, other)
if not self.allows_any(other) or not other.allows_any(self):
return EmptyConstraint()

return EmptyConstraint()
return MultiConstraint(self, other)

return other.intersect(self)

Expand All @@ -146,16 +172,25 @@ def union(self, other: BaseConstraint) -> BaseConstraint:
if other == self:
return self

if self.operator == "!=" and other.operator == "==" and self.allows(other):
if self.allows_all(other):
return self

if other.operator == "!=" and self.operator == "==" and other.allows(self):
if other.allows_all(self):
return other

if other.operator == "==" and self.operator == "==":
return UnionConstraint(self, other)
ops = {self.operator, other.operator}
if (
(ops in ({"!="}, {"not in"}))
or (
ops in ({"in", "!="}, {"in", "not in"})
and (self.operator == "in" and self.value in other.value)
or (other.operator == "in" and other.value in self.value)
)
or self.invert() == other
):
npapapietro marked this conversation as resolved.
Show resolved Hide resolved
return AnyConstraint()

return AnyConstraint()
return UnionConstraint(self, other)

# to preserve order (functionally not necessary)
if isinstance(other, UnionConstraint):
Expand All @@ -179,5 +214,7 @@ def __hash__(self) -> int:
return hash((self._operator, self._value))

def __str__(self) -> str:
if self._operator in {"in", "not in"}:
return f"'{self._value}' {self._operator}"
op = self._operator if self._operator != "==" else ""
return f"{op}{self._value}"
23 changes: 18 additions & 5 deletions src/poetry/core/constraints/generic/parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,15 @@


BASIC_CONSTRAINT = re.compile(r"^(!?==?)?\s*([^\s]+?)\s*$")
STR_CMP_CONSTRAINT = re.compile(
r"""(?ix)^ # case insensitive and verbose mode
(?P<quote>['"]) # Single or double quotes
(?P<value>.+?) # The value itself inside quotes
\1 # Closing single of double quote
\s* # Space
(?P<op>(not\sin|in)) # Literal match of 'in' or 'not in'
$"""
)


@functools.lru_cache(maxsize=None)
Expand All @@ -26,9 +35,7 @@ def parse_constraint(constraints: str) -> BaseConstraint:
or_constraints = re.split(r"\s*\|\|?\s*", constraints.strip())
or_groups = []
for constraints in or_constraints:
and_constraints = re.split(
r"(?<!^)(?<![=>< ,]) *(?<!-)[, ](?!-) *(?!,|$)", constraints
)
and_constraints = re.split(r"\s*,\s*", constraints)
constraint_objects = []

if len(and_constraints) > 1:
Expand All @@ -53,9 +60,15 @@ def parse_constraint(constraints: str) -> BaseConstraint:


def parse_single_constraint(constraint: str) -> Constraint:
# string comparator
if m := STR_CMP_CONSTRAINT.match(constraint):
op = m.group("op")
value = m.group("value").strip()
return Constraint(value, op)

# Basic comparator
m = BASIC_CONSTRAINT.match(constraint)
if m:

if m := BASIC_CONSTRAINT.match(constraint):
op = m.group(1)
if op is None:
op = "=="
Expand Down
50 changes: 44 additions & 6 deletions src/poetry/core/constraints/version/parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,16 @@ 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_marker_version_constraint(
constraints: str, *, pep440: bool = True
) -> VersionConstraint:
return _parse_constraint(
constraints=constraints, is_marker_constraint=True, pep440=pep440
)


def _parse_constraint(
constraints: str, *, is_marker_constraint: bool = False
constraints: str, *, is_marker_constraint: bool = False, pep440: bool = True
) -> VersionConstraint:
if constraints == "*":
from poetry.core.constraints.version.version_range import VersionRange
Expand All @@ -46,13 +50,17 @@ def _parse_constraint(
for constraint in and_constraints:
constraint_objects.append(
parse_single_constraint(
constraint, is_marker_constraint=is_marker_constraint
constraint,
is_marker_constraint=is_marker_constraint,
pep440=pep440,
)
)
else:
constraint_objects.append(
parse_single_constraint(
and_constraints[0], is_marker_constraint=is_marker_constraint
and_constraints[0],
is_marker_constraint=is_marker_constraint,
pep440=pep440,
)
)

Expand All @@ -74,9 +82,10 @@ def _parse_constraint(


def parse_single_constraint(
constraint: str, *, is_marker_constraint: bool = False
constraint: str, *, is_marker_constraint: bool = False, pep440: bool = True
) -> VersionConstraint:
from poetry.core.constraints.version.patterns import BASIC_CONSTRAINT
from poetry.core.constraints.version.patterns import BASIC_RELEASE_CONSTRAINT
from poetry.core.constraints.version.patterns import CARET_CONSTRAINT
from poetry.core.constraints.version.patterns import TILDE_CONSTRAINT
from poetry.core.constraints.version.patterns import TILDE_PEP440_CONSTRAINT
Expand Down Expand Up @@ -185,6 +194,35 @@ def parse_single_constraint(

return version

# These below should be reserved for comparing non python packages such as OS
# versions using `platform_release`
if not pep440 and (m := BASIC_RELEASE_CONSTRAINT.match(constraint)):
op = m.group("op")
release_string = m.group("release")
build = m.group("build")

try:
version = Version(
release=Version.parse(release_string).release,
local=build,
)
except InvalidVersion as e:
raise ParseConstraintError(
f"Could not parse version constraint: {constraint}"
) from e

if op == "<":
return VersionRange(max=version)
if op == "<=":
return VersionRange(max=version, include_max=True)
if op == ">":
return VersionRange(min=version)
if op == ">=":
return VersionRange(min=version, include_min=True)
if op == "!=":
return VersionUnion(VersionRange(max=version), VersionRange(min=version))
return version

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


Expand Down
14 changes: 14 additions & 0 deletions src/poetry/core/constraints/version/patterns.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,3 +26,17 @@
rf"^(?P<op><>|!=|>=?|<=?|==?)?\s*(?P<version>{VERSION_PATTERN}|dev)(?P<wildcard>\.\*)?$",
re.VERBOSE | re.IGNORECASE,
)

RELEASE_PATTERN = r"""
(?P<release>[0-9]+(?:\.[0-9]+)*)
(?:(\+|-)(?P<build>
[0-9a-zA-Z-]+
(?:\.[0-9a-zA-Z-]+)*
))?
"""

# pattern for non Python versions such as OS versions in `platform_release`
BASIC_RELEASE_CONSTRAINT = re.compile(
rf"^(?P<op><>|!=|>=?|<=?|==?)?\s*(?P<version>{RELEASE_PATTERN})$",
re.VERBOSE | re.IGNORECASE,
)
Loading
Loading