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

Generic constraint intersection and union #515

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
29 changes: 20 additions & 9 deletions src/poetry/core/constraints/generic/constraint.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from __future__ import annotations

import operator
import warnings

from poetry.core.constraints.generic.any_constraint import AnyConstraint
from poetry.core.constraints.generic.base_constraint import BaseConstraint
Expand All @@ -15,17 +16,27 @@ class Constraint(BaseConstraint):

_trans_op_int = {OP_EQ: "==", OP_NE: "!="}

def __init__(self, version: str, operator: str = "==") -> None:
def __init__(self, value: str, operator: str = "==") -> None:
if operator == "=":
operator = "=="

self._version = version
self._value = value
self._operator = operator
self._op = self._trans_op_str[operator]

@property
def value(self) -> str:
return self._value

@property
def version(self) -> str:
return self._version
warnings.warn(
Secrus marked this conversation as resolved.
Show resolved Hide resolved
"The property 'version' is deprecated and will be removed. "
"Please use the property 'value' instead.",
DeprecationWarning,
stacklevel=2,
)
return self.value

@property
def operator(self) -> str:
Expand All @@ -41,7 +52,7 @@ def allows(self, other: BaseConstraint) -> bool:
is_other_non_equal_op = other.operator == "!="

if is_equal_op and is_other_equal_op:
return self._version == other.version
return self._value == other.value

if (
is_equal_op
Expand All @@ -51,7 +62,7 @@ def allows(self, other: BaseConstraint) -> bool:
or is_non_equal_op
and is_other_non_equal_op
):
return self._version != other.version
return self._value != other.value

return False

Expand All @@ -67,7 +78,7 @@ def allows_any(self, other: BaseConstraint) -> bool:
is_other_non_equal_op = other.operator == "!="

if is_non_equal_op and is_other_non_equal_op:
return self._version != other.version
return self._value != other.value

return other.allows(self)

Expand Down Expand Up @@ -127,11 +138,11 @@ def __eq__(self, other: object) -> bool:
if not isinstance(other, Constraint):
return NotImplemented

return (self.version, self.operator) == (other.version, other.operator)
return (self.value, self.operator) == (other.value, other.operator)

def __hash__(self) -> int:
return hash((self._operator, self._version))
return hash((self._operator, self._value))

def __str__(self) -> str:
op = self._operator if self._operator != "==" else ""
return f"{op}{self._version}"
return f"{op}{self._value}"
3 changes: 3 additions & 0 deletions src/poetry/core/constraints/generic/empty_constraint.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@ def allows_any(self, other: BaseConstraint) -> bool:
def intersect(self, other: BaseConstraint) -> BaseConstraint:
return self

def union(self, other: BaseConstraint) -> BaseConstraint:
return other

def difference(self, other: BaseConstraint) -> BaseConstraint:
return self

Expand Down
35 changes: 29 additions & 6 deletions src/poetry/core/constraints/generic/multi_constraint.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
from __future__ import annotations

from poetry.core.constraints.generic import AnyConstraint
from poetry.core.constraints.generic import EmptyConstraint
from poetry.core.constraints.generic.base_constraint import BaseConstraint
from poetry.core.constraints.generic.constraint import Constraint

Expand Down Expand Up @@ -62,13 +64,34 @@ def allows_any(self, other: BaseConstraint) -> bool:

def intersect(self, other: BaseConstraint) -> BaseConstraint:
if not isinstance(other, Constraint):
raise ValueError("Unimplemented constraint intersection")
return other.intersect(self)

constraints = self._constraints
if other not in constraints:
constraints += (other,)
else:
constraints = (other,)
if other in self._constraints:
return self

if other.value in (c.value for c in self._constraints):
# same value but different operator, e.g. '== "linux"' and '!= "linux"'
return EmptyConstraint()

if other.operator == "==":
return other

return MultiConstraint(*self._constraints, other)

def union(self, other: BaseConstraint) -> BaseConstraint:
if not isinstance(other, Constraint):
return other.union(self)

if other in self._constraints:
return other

if other.value not in (c.value for c in self._constraints):
if other.operator == "!=":
return AnyConstraint()

return self

constraints = [c for c in self._constraints if c.value != other.value]

if len(constraints) == 1:
return constraints[0]
Expand Down
62 changes: 43 additions & 19 deletions src/poetry/core/constraints/generic/union_constraint.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from __future__ import annotations

from poetry.core.constraints.generic import AnyConstraint
from poetry.core.constraints.generic.base_constraint import BaseConstraint
from poetry.core.constraints.generic.constraint import Constraint
from poetry.core.constraints.generic.empty_constraint import EmptyConstraint
Expand Down Expand Up @@ -71,21 +72,13 @@ def intersect(self, other: BaseConstraint) -> BaseConstraint:
return other

if isinstance(other, Constraint):
if self.allows(other):
return other
# (A or B) and C => (A and C) or (B and C)
# just a special case of UnionConstraint
other = UnionConstraint(other)

return EmptyConstraint()

# Two remaining cases: an intersection with another union, or an intersection
# with a multi.
#
# In the first case:
# (A or B) and (C or D) => (A and C) or (A and D) or (B and C) or (B and D)
#
# In the second case:
# (A or B) and (C and D) => (A and C and D) or (B and C and D)
new_constraints = []
if isinstance(other, UnionConstraint):
# (A or B) and (C or D) => (A and C) or (A and D) or (B and C) or (B and D)
for our_constraint in self._constraints:
for their_constraint in other.constraints:
intersection = our_constraint.intersect(their_constraint)
Expand All @@ -98,6 +91,7 @@ def intersect(self, other: BaseConstraint) -> BaseConstraint:

else:
assert isinstance(other, MultiConstraint)
# (A or B) and (C and D) => (A and C and D) or (B and C and D)

for our_constraint in self._constraints:
intersection = our_constraint
Expand All @@ -115,15 +109,45 @@ def intersect(self, other: BaseConstraint) -> BaseConstraint:

return UnionConstraint(*new_constraints)

def union(self, other: BaseConstraint) -> UnionConstraint:
if not isinstance(other, Constraint):
raise ValueError("Unimplemented constraint union")
def union(self, other: BaseConstraint) -> BaseConstraint:
if other.is_any():
return other

if other.is_empty():
return self

if isinstance(other, Constraint):
# (A or B) or C => A or B or C
# just a special case of UnionConstraint
other = UnionConstraint(other)

new_constraints: list[BaseConstraint] = []
if isinstance(other, UnionConstraint):
# (A or B) or (C or D) => A or B or C or D
for our_constraint in self._constraints:
for their_constraint in other.constraints:
union = our_constraint.union(their_constraint)
if union.is_any():
return AnyConstraint()
if isinstance(union, Constraint):
if union not in new_constraints:
new_constraints.append(union)
else:
if our_constraint not in new_constraints:
new_constraints.append(our_constraint)
if their_constraint not in new_constraints:
new_constraints.append(their_constraint)

else:
assert isinstance(other, MultiConstraint)
# (A or B) or (C and D) => nothing to do

constraints = self._constraints
if other not in self._constraints:
constraints += (other,)
new_constraints = [*self._constraints, other]

return UnionConstraint(*constraints)
if len(new_constraints) == 1:
return new_constraints[0]

return UnionConstraint(*new_constraints)

def __eq__(self, other: object) -> bool:
if not isinstance(other, UnionConstraint):
Expand Down
Loading