Skip to content

Commit

Permalink
Properly propagate dependency markers (#1829)
Browse files Browse the repository at this point in the history
  • Loading branch information
sdispater authored Jan 10, 2020
1 parent 10e471a commit 6e053e5
Show file tree
Hide file tree
Showing 9 changed files with 337 additions and 20 deletions.
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(
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

0 comments on commit 6e053e5

Please sign in to comment.