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

Fix handling of markers when resolving #2342

Merged
merged 2 commits into from
Apr 24, 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
402 changes: 130 additions & 272 deletions poetry.lock

Large diffs are not rendered by default.

6 changes: 6 additions & 0 deletions poetry/console/commands/debug/resolve.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ class DebugResolveCommand(InitCommand):
def handle(self):
from poetry.io.null_io import NullIO
from poetry.core.packages import ProjectPackage
from poetry.installation.installer import Installer
from poetry.puzzle import Solver
from poetry.repositories.pool import Pool
from poetry.repositories.repository import Repository
Expand Down Expand Up @@ -114,10 +115,15 @@ def handle(self):
pool.add_repository(locked_repository)

with package.with_python_versions(current_python_version):
installer = Installer(NullIO(), env, package, self.poetry.locker, pool)
solver = Solver(package, pool, Repository(), Repository(), NullIO())
ops = solver.solve()
installer._filter_operations(ops, Repository())

for op in ops:
if self.option("install") and op.skipped:
continue

pkg = op.package
row = [
"<c1>{}</c1>".format(pkg.name),
Expand Down
28 changes: 0 additions & 28 deletions poetry/installation/installer.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
from typing import Union

from clikit.api.io import IO
from clikit.io import NullIO

from poetry.core.packages.package import Package
from poetry.core.semver import parse_constraint
Expand Down Expand Up @@ -197,33 +196,6 @@ def _do_install(self, local_repo):
root = root.clone()
del root.dev_requires[:]

with root.with_python_versions(
".".join([str(i) for i in self._env.version_info[:3]])
):
# We resolve again by only using the lock file
pool = Pool(ignore_repository_names=True)

# Making a new repo containing the packages
# newly resolved and the ones from the current lock file
repo = Repository()
for package in local_repo.packages + locked_repository.packages:
if not repo.has_package(package):
repo.add_package(package)

pool.add_repository(repo)

# We whitelist all packages to be sure
# that the latest ones are picked up
whitelist = []
for pkg in locked_repository.packages:
whitelist.append(pkg.name)

solver = Solver(
root, pool, self._installed_repository, locked_repository, NullIO()
)

ops = solver.solve(use_latest=whitelist)

# We need to filter operations so that packages
# not compatible with the current system,
# or optional and not requested, are dropped
Expand Down
18 changes: 9 additions & 9 deletions poetry/puzzle/exceptions.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,3 @@
class CompatibilityError(Exception):
def __init__(self, *constraints):
self._constraints = list(constraints)

@property
def constraints(self):
return self._constraints


class SolverProblemError(Exception):
def __init__(self, error):
self._error = error
Expand All @@ -16,3 +7,12 @@ def __init__(self, error):
@property
def error(self):
return self._error


class OverrideNeeded(Exception):
def __init__(self, *overrides):
self._overrides = overrides

@property
def overrides(self):
return self._overrides
113 changes: 96 additions & 17 deletions poetry/puzzle/provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@
from poetry.utils.setup_reader import SetupReader
from poetry.utils.toml_file import TomlFile

from .exceptions import CompatibilityError
from .exceptions import OverrideNeeded


logger = logging.getLogger(__name__)
Expand All @@ -72,6 +72,7 @@ def __init__(self, package, pool, io): # type: (Package, Pool, Any) -> None
self._search_for = {}
self._is_debugging = self._io.is_debug() or self._io.is_very_verbose()
self._in_progress = False
self._overrides = {}

@property
def pool(self): # type: () -> Pool
Expand All @@ -88,6 +89,9 @@ def name_for_locking_dependency_source(self): # type: () -> str
def is_debugging(self):
return self._is_debugging

def set_overrides(self, overrides):
self._overrides = overrides

def name_for(self, dependency): # type: (Dependency) -> str
"""
Returns the name for the given dependency.
Expand Down Expand Up @@ -514,13 +518,28 @@ def incompatibilities_for(
)
]

dependencies = [
_dependencies = [
dep
for dep in dependencies
if dep.name not in self.UNSAFE_PACKAGES
and self._package.python_constraint.allows_any(dep.python_constraint)
]

overrides = self._overrides.get(package, {})
dependencies = []
overridden = []
for dep in _dependencies:
if dep.name in overrides:
if dep.name in overridden:
continue

dependencies.append(overrides[dep.name])
overridden.append(dep.name)

continue

dependencies.append(dep)

return [
Incompatibility(
[Term(package.to_dependency(), True), Term(dep, False)],
Expand Down Expand Up @@ -554,12 +573,28 @@ def complete_package(
else:
requires = package.requires

dependencies = [
_dependencies = [
r
for r in requires
if self._package.python_constraint.allows_any(r.python_constraint)
and r.name not in self.UNSAFE_PACKAGES
]

overrides = self._overrides.get(package, {})
dependencies = []
overridden = []
for dep in _dependencies:
if dep.name in overrides:
if dep.name in overridden:
continue

dependencies.append(overrides[dep.name])
overridden.append(dep.name)

continue

dependencies.append(dep)

# Searching for duplicate dependencies
#
# If the duplicate dependencies have the same constraint,
Expand Down Expand Up @@ -651,26 +686,70 @@ def complete_package(
continue

# At this point, we raise an exception that will
# tell the solver to enter compatibility mode
# which means it will resolve for subsets
# Python constraints
# tell the solver to make new resolutions with specific overrides.
#
# For instance, if the foo (1.2.3) package has the following dependencies:
# - bar (>=2.0) ; python_version >= "3.6"
# - bar (<2.0) ; python_version < "3.6"
#
# For instance, if our root package requires Python ~2.7 || ^3.6
# And we have one dependency that requires Python <3.6
# and the other Python >=3.6 than the solver will solve
# dependencies for Python >=2.7,<2.8 || >=3.4,<3.6
# and Python >=3.6,<4.0
python_constraints = []
# then the solver will need to make two new resolutions
# with the following overrides:
# - {<Package foo (1.2.3): {"bar": <Dependency bar (>=2.0)>}
# - {<Package foo (1.2.3): {"bar": <Dependency bar (<2.0)>}
markers = []
for constraint, _deps in by_constraint.items():
python_constraints.append(_deps[0].python_versions)
markers.append(_deps[0].marker)

_deps = [str(_dep[0]) for _dep in by_constraint.values()]
_deps = [_dep[0] for _dep in by_constraint.values()]
self.debug(
"<warning>Different requirements found for {}.</warning>".format(
", ".join(_deps[:-1]) + " and " + _deps[-1]
", ".join(
"<c1>{}</c1> <fg=default>(<c2>{}</c2>)</> with markers <b>{}</b>".format(
d.name,
d.pretty_constraint,
d.marker if not d.marker.is_any() else "*",
)
for d in _deps[:-1]
)
+ " and "
+ "<c1>{}</c1> <fg=default>(<c2>{}</c2>)</> with markers <b>{}</b>".format(
_deps[-1].name,
_deps[-1].pretty_constraint,
_deps[-1].marker if not _deps[-1].marker.is_any() else "*",
)
)
)
raise CompatibilityError(*python_constraints)

# We need to check if one of the duplicate dependencies
# has no markers. If there is one, we need to change its
# environment markers to the inverse of the union of the
# other dependencies markers.
# For instance, if we have the following dependencies:
# - ipython
# - ipython (1.2.4) ; implementation_name == "pypy"
#
# the marker for `ipython` will become `implementation_name != "pypy"`.
any_markers_dependencies = [d for d in _deps if d.marker.is_any()]
other_markers_dependencies = [d for d in _deps if not d.marker.is_any()]

if any_markers_dependencies:
marker = other_markers_dependencies[0].marker
for other_dep in other_markers_dependencies[1:]:
marker = marker.union(other_dep.marker)

for i, d in enumerate(_deps):
if d.marker.is_any():
_deps[i].marker = marker.invert()

overrides = []
for _dep in _deps:
current_overrides = self._overrides.copy()
package_overrides = current_overrides.get(package, {})
package_overrides.update({_dep.name: _dep})
current_overrides.update({package: package_overrides})
overrides.append(current_overrides)

raise OverrideNeeded(*overrides)

# Modifying dependencies as needed
clean_dependencies = []
Expand Down Expand Up @@ -724,7 +803,7 @@ def debug(self, message, depth=0):
m2 = re.match(r"(.+?) \((.+?)\)", m.group(1))
if m2:
name = m2.group(1)
version = " (<b>{}</b>)".format(m2.group(2))
version = " (<c2>{}</c2>)".format(m2.group(2))
else:
name = m.group(1)
version = ""
Expand Down
63 changes: 29 additions & 34 deletions poetry/puzzle/solver.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,12 @@
from typing import List

from poetry.core.packages import Package
from poetry.core.semver import parse_constraint
from poetry.core.version.markers import AnyMarker
from poetry.mixology import resolve_version
from poetry.mixology.failure import SolveFailure
from poetry.packages import DependencyPackage

from .exceptions import CompatibilityError
from .exceptions import OverrideNeeded
from .exceptions import SolverProblemError
from .operations import Install
from .operations import Uninstall
Expand All @@ -28,23 +27,23 @@ def __init__(self, package, pool, installed, locked, io):
self._locked = locked
self._io = io
self._provider = Provider(self._package, self._pool, self._io)
self._branches = []
self._overrides = []

def solve(self, use_latest=None): # type: (...) -> List[Operation]
with self._provider.progress():
start = time.time()
packages, depths = self._solve(use_latest=use_latest)
end = time.time()

if len(self._branches) > 1:
if len(self._overrides) > 1:
self._provider.debug(
"Complete version solving took {:.3f} seconds for {} branches".format(
end - start, len(self._branches[1:])
"Complete version solving took {:.3f} seconds with {} overrides".format(
end - start, len(self._overrides)
)
)
self._provider.debug(
"Resolved for branches: {}".format(
", ".join("({})".format(b) for b in self._branches[1:])
"Resolved with overrides: {}".format(
", ".join("({})".format(b) for b in self._overrides)
)
)

Expand Down Expand Up @@ -135,42 +134,40 @@ def solve(self, use_latest=None): # type: (...) -> List[Operation]
),
)

def solve_in_compatibility_mode(self, constraints, use_latest=None):
def solve_in_compatibility_mode(self, overrides, use_latest=None):
locked = {}
for package in self._locked.packages:
locked[package.name] = DependencyPackage(package.to_dependency(), package)

packages = []
depths = []
for constraint in constraints:
constraint = parse_constraint(constraint)
intersection = constraint.intersect(self._package.python_constraint)

for override in overrides:
self._provider.debug(
"<comment>Retrying dependency resolution "
"for Python ({}).</comment>".format(intersection)
"with the following overrides ({}).</comment>".format(override)
)
with self._package.with_python_versions(str(intersection)):
_packages, _depths = self._solve(use_latest=use_latest)
for index, package in enumerate(_packages):
if package not in packages:
packages.append(package)
depths.append(_depths[index])
continue
else:
idx = packages.index(package)
pkg = packages[idx]
depths[idx] = max(depths[idx], _depths[index])
pkg.marker = pkg.marker.union(package.marker)
self._provider.set_overrides(override)
_packages, _depths = self._solve(use_latest=use_latest)
for index, package in enumerate(_packages):
if package not in packages:
packages.append(package)
depths.append(_depths[index])
continue
else:
idx = packages.index(package)
pkg = packages[idx]
depths[idx] = max(depths[idx], _depths[index])
pkg.marker = pkg.marker.union(package.marker)

for dep in package.requires:
if dep not in pkg.requires:
pkg.requires.append(dep)
for dep in package.requires:
if dep not in pkg.requires:
pkg.requires.append(dep)

return packages, depths

def _solve(self, use_latest=None):
self._branches.append(self._package.python_versions)
if self._provider._overrides:
self._overrides.append(self._provider._overrides)

locked = {}
for package in self._locked.packages:
Expand All @@ -182,10 +179,8 @@ def _solve(self, use_latest=None):
)

packages = result.packages
except CompatibilityError as e:
return self.solve_in_compatibility_mode(
e.constraints, use_latest=use_latest
)
except OverrideNeeded as e:
return self.solve_in_compatibility_mode(e.overrides, use_latest=use_latest)
except SolveFailure as e:
raise SolverProblemError(e)

Expand Down
Loading