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

Properly propagate dependency markers #1829

Merged
merged 1 commit into from
Jan 10, 2020
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
12 changes: 12 additions & 0 deletions poetry/packages/dependency.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ def __init__(
self._python_constraint = parse_constraint("*")
self._transitive_python_versions = None
self._transitive_python_constraint = None
self._transitive_marker = None

self._extras = []
self._in_extras = []
Expand Down Expand Up @@ -117,6 +118,17 @@ def transitive_python_versions(self, value):
self._transitive_python_versions = value
self._transitive_python_constraint = parse_constraint(value)

@property
def transitive_marker(self):
if self._transitive_marker is None:
return self.marker

return self._transitive_marker

@transitive_marker.setter
def transitive_marker(self, value):
self._transitive_marker = value

@property
def python_constraint(self):
return self._python_constraint
Expand Down
68 changes: 68 additions & 0 deletions poetry/packages/utils/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,13 @@
from poetry.packages.constraints.constraint import Constraint
from poetry.packages.constraints.multi_constraint import MultiConstraint
from poetry.packages.constraints.union_constraint import UnionConstraint
from poetry.semver import EmptyConstraint
from poetry.semver import Version
from poetry.semver import VersionConstraint
from poetry.semver import VersionRange
from poetry.semver import VersionUnion
from poetry.semver import parse_constraint
from poetry.version.markers import BaseMarker
from poetry.version.markers import MarkerUnion
from poetry.version.markers import MultiMarker
from poetry.version.markers import SingleMarker
Expand Down Expand Up @@ -236,3 +241,66 @@ def create_nested_marker(name, constraint):
marker = '{} {} "{}"'.format(name, op, version)

return marker


def get_python_constraint_from_marker(
marker,
): # type: (BaseMarker) -> VersionConstraint
python_marker = marker.only("python_version")
if python_marker.is_any():
return VersionRange()

if python_marker.is_empty():
return EmptyConstraint()

markers = convert_markers(marker)

ors = []
for or_ in markers["python_version"]:
ands = []
for op, version in or_:
# Expand python version
if op == "==":
version = "~" + version
op = ""
elif op == "!=":
version += ".*"
elif op in ("<=", ">"):
parsed_version = Version.parse(version)
if parsed_version.precision == 1:
if op == "<=":
op = "<"
version = parsed_version.next_major.text
elif op == ">":
op = ">="
version = parsed_version.next_major.text
elif parsed_version.precision == 2:
if op == "<=":
op = "<"
version = parsed_version.next_minor.text
elif op == ">":
op = ">="
version = parsed_version.next_minor.text
elif op in ("in", "not in"):
versions = []
for v in re.split("[ ,]+", version):
split = v.split(".")
if len(split) in [1, 2]:
split.append("*")
op_ = "" if op == "in" else "!="
else:
op_ = "==" if op == "in" else "!="

versions.append(op_ + ".".join(split))

glue = " || " if op == "in" else ", "
if versions:
ands.append(glue.join(versions))

continue

ands.append("{}{}".format(op, version))

ors.append(" ".join(ands))

return parse_constraint(" || ".join(ors))
26 changes: 20 additions & 6 deletions poetry/puzzle/provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
from poetry.packages import URLDependency
from poetry.packages import VCSDependency
from poetry.packages import dependency_from_pep_508
from poetry.packages.utils.utils import get_python_constraint_from_marker
from poetry.repositories import Pool
from poetry.utils._compat import PY35
from poetry.utils._compat import OrderedDict
Expand Down Expand Up @@ -489,14 +490,15 @@ def incompatibilities_for(
if not package.python_constraint.allows_all(
self._package.python_constraint
):
intersection = package.python_constraint.intersect(
package.dependency.transitive_python_constraint
transitive_python_constraint = get_python_constraint_from_marker(
kasteph marked this conversation as resolved.
Show resolved Hide resolved
package.dependency.transitive_marker
)
difference = package.dependency.transitive_python_constraint.difference(
intersection
intersection = package.python_constraint.intersect(
transitive_python_constraint
)
difference = transitive_python_constraint.difference(intersection)
if (
package.dependency.transitive_python_constraint.is_any()
transitive_python_constraint.is_any()
or self._package.python_constraint.intersect(
package.dependency.python_constraint
).is_empty()
Expand Down Expand Up @@ -673,12 +675,24 @@ def complete_package(
# Modifying dependencies as needed
clean_dependencies = []
for dep in dependencies:
if not package.dependency.transitive_marker.without_extras().is_any():
marker_intersection = package.dependency.transitive_marker.without_extras().intersect(
dep.marker.without_extras()
)
if marker_intersection.is_empty():
# The dependency is not needed, since the markers specified
# for the current package selection are not compatible with
# the markers for the current dependency, so we skip it
continue

dep.transitive_marker = marker_intersection

if not package.dependency.python_constraint.is_any():
python_constraint_intersection = dep.python_constraint.intersect(
package.dependency.python_constraint
)
if python_constraint_intersection.is_empty():
# This depencency is not needed under current python constraint.
# This dependency is not needed under current python constraint.
continue
dep.transitive_python_versions = str(python_constraint_intersection)

Expand Down
2 changes: 1 addition & 1 deletion poetry/puzzle/solver.py
Original file line number Diff line number Diff line change
Expand Up @@ -225,7 +225,7 @@ def _build_graph(
intersection = (
previous["marker"]
.without_extras()
.intersect(previous_dep.marker.without_extras())
.intersect(previous_dep.transitive_marker.without_extras())
)
intersection = intersection.intersect(package.marker.without_extras())

Expand Down
90 changes: 81 additions & 9 deletions poetry/version/markers.py
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,12 @@ def validate(self, environment): # type: (Dict[str, Any]) -> bool
def without_extras(self): # type: () -> BaseMarker
raise NotImplementedError()

def exclude(self, marker_name): # type: (str) -> BaseMarker
raise NotImplementedError()

def only(self, marker_name): # type: (str) -> BaseMarker
raise NotImplementedError()

def __repr__(self):
return "<{} {}>".format(self.__class__.__name__, str(self))

Expand All @@ -198,6 +204,12 @@ def validate(self, environment):
def without_extras(self):
return self

def exclude(self, marker_name): # type: (str) -> AnyMarker
return self

def only(self, marker_name): # type: (str) -> AnyMarker
return self

def __str__(self):
return ""

Expand Down Expand Up @@ -233,6 +245,12 @@ def validate(self, environment):
def without_extras(self):
return self

def exclude(self, marker_name): # type: (str) -> EmptyMarker
return self

def only(self, marker_name): # type: (str) -> EmptyMarker
return self

def __str__(self):
return "<empty>"

Expand Down Expand Up @@ -361,11 +379,20 @@ def validate(self, environment):
return self._constraint.allows(self._parser(environment[self._name]))

def without_extras(self):
if self.name == "extra":
return self.exclude("extra")

def exclude(self, marker_name): # type: (str) -> BaseMarker
if self.name == marker_name:
return AnyMarker()

return self

def only(self, marker_name): # type: (str) -> BaseMarker
if self.name != marker_name:
return EmptyMarker()

return self

def __eq__(self, other):
if not isinstance(other, SingleMarker):
return False
Expand Down Expand Up @@ -410,7 +437,7 @@ def of(cls, *markers):
markers = _flatten_markers(markers, MultiMarker)

for marker in markers:
if marker in new_markers or marker.is_empty():
if marker in new_markers:
continue

if isinstance(marker, SingleMarker):
Expand All @@ -426,11 +453,9 @@ def of(cls, *markers):
intersection = mark.constraint.intersect(marker.constraint)
if intersection == mark.constraint:
intersected = True
break
elif intersection == marker.constraint:
new_markers[i] = marker
intersected = True
break
elif intersection.is_empty():
return EmptyMarker()

Expand All @@ -439,9 +464,12 @@ def of(cls, *markers):

new_markers.append(marker)

if not new_markers:
if any(m.is_empty() for m in new_markers) or not new_markers:
return EmptyMarker()

if len(new_markers) == 1 and new_markers[0].is_any():
return AnyMarker()

return MultiMarker(*new_markers)

@property
Expand Down Expand Up @@ -473,10 +501,32 @@ def validate(self, environment):
return True

def without_extras(self):
return self.exclude("extra")

def exclude(self, marker_name): # type: (str) -> BaseMarker
new_markers = []

for m in self._markers:
if isinstance(m, SingleMarker) and m.name == marker_name:
# The marker is not relevant since it must be excluded
continue

marker = m.exclude(marker_name)

if not marker.is_empty():
new_markers.append(marker)

return self.of(*new_markers)

def only(self, marker_name): # type: (str) -> BaseMarker
new_markers = []

for m in self._markers:
marker = m.without_extras()
if isinstance(m, SingleMarker) and m.name != marker_name:
# The marker is not relevant since it's not one we want
continue

marker = m.only(marker_name)

if not marker.is_empty():
new_markers.append(marker)
Expand Down Expand Up @@ -550,7 +600,7 @@ def of(cls, *markers): # type: (tuple) -> MarkerUnion

markers.append(marker)

if len(markers) == 1 and markers[0].is_any():
if any(m.is_any() for m in markers):
return AnyMarker()

return MarkerUnion(*markers)
Expand Down Expand Up @@ -604,15 +654,37 @@ def validate(self, environment):
return False

def without_extras(self):
return self.exclude("extra")

def exclude(self, marker_name): # type: (str) -> BaseMarker
new_markers = []

for m in self._markers:
marker = m.without_extras()
if isinstance(m, SingleMarker) and m.name == marker_name:
# The marker is not relevant since it must be excluded
continue

marker = m.exclude(marker_name)

if not marker.is_empty():
new_markers.append(marker)

return MarkerUnion(*new_markers)
return self.of(*new_markers)

def only(self, marker_name): # type: (str) -> BaseMarker
new_markers = []

for m in self._markers:
if isinstance(m, SingleMarker) and m.name != marker_name:
# The marker is not relevant since it's not one we want
continue

marker = m.only(marker_name)

if not marker.is_empty():
new_markers.append(marker)

return self.of(*new_markers)

def __eq__(self, other):
if not isinstance(other, MarkerUnion):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ C = "1.5"
[[package]]
name = "C"
version = "1.5"
marker = "python_version >= \"2.7\""
description = ""
category = "main"
optional = false
Expand Down
8 changes: 4 additions & 4 deletions tests/installation/test_installer.py
Original file line number Diff line number Diff line change
Expand Up @@ -1183,8 +1183,8 @@ def test_run_install_duplicate_dependencies_different_constraints_with_lock_upda
"checksum": [],
"dependencies": {
"B": [
{"version": "^1.0", "python": "<4.0"},
{"version": "^2.0", "python": ">=4.0"},
{"version": "^1.0", "python": "<2.7"},
{"version": "^2.0", "python": ">=2.7"},
]
},
},
Expand All @@ -1197,7 +1197,7 @@ def test_run_install_duplicate_dependencies_different_constraints_with_lock_upda
"python-versions": "*",
"checksum": [],
"dependencies": {"C": "1.2"},
"requirements": {"python": "<4.0"},
"requirements": {"python": "<2.7"},
},
{
"name": "B",
Expand All @@ -1208,7 +1208,7 @@ def test_run_install_duplicate_dependencies_different_constraints_with_lock_upda
"python-versions": "*",
"checksum": [],
"dependencies": {"C": "1.5"},
"requirements": {"python": ">=4.0"},
"requirements": {"python": ">=2.7"},
},
{
"name": "C",
Expand Down
Loading