Task Summary
Instruction pytest failed in 01:23
Details
✅ 00:03 clone
✅ 01:07 bootstrap_poetry
✅ 00:15 setup_environment
❌ 01:23 pytest
except OverrideNeeded as e:
return self._solve_in_compatibility_mode(e.overrides)
except SolveFailure as e:
> raise SolverProblemError(e)
E poetry.puzzle.exceptions.SolverProblemError: Because demo[demo-extra-two] (1.0.0) depends on transitive-dep (1.2.0)
E and demo[demo-extra-one] (1.0.0) depends on transitive-dep (1.1.0), demo[demo-extra-two] (1.0.0) is incompatible with demo[demo-extra-one] (1.0.0).
E So, because root depends on both demo[demo-extra-one] (1.0.0) and demo[demo-extra-two] (1.0.0), version solving failed.
/tmp/cirrus-ci-build/src/poetry/puzzle/solver.py:167: SolverProblemError
-------------- generated xml file: /tmp/cirrus-ci-build/junit.xml --------------
=========================== short test summary info ============================
FAILED tests/puzzle/test_solver.py::test_solver_resolves_duplicate_dependency_in_root_extra[True] - AssertionError: assert [{'job': 'ins...pped': False}] == [{'job': 'ins...pped': False}]
Left contains one more item: {'job': 'install', 'package': Package('a', '2.0'), 'skipped': False}
Full diff:
[
{
'job': 'install',
'package': Package('a', '1.0'),
'skipped': False,
},
+ {
+ 'job': 'install',
+ 'package': Package('a', '2.0'),
+ 'skipped': False,
+ },
]
FAILED tests/puzzle/test_solver.py::test_solver_resolves_duplicate_dependency_in_root_extra[False] - AssertionError: assert [{'job': 'ins...pped': False}] == [{'job': 'ins...pped': False}]
At index 0 diff: {'job': 'install', 'package': Package('a', '1.0'), 'skipped': False} != {'job': 'install', 'package': Package('a', '2.0'), 'skipped': False}
Left contains one more item: {'job': 'install', 'package': Package('a', '2.0'), 'skipped': False}
Full diff:
[
+ {
+ 'job': 'install',
+ 'package': Package('a', '1.0'),
+ 'skipped': False,
+ },
{
'job': 'install',
'package': Package('a', '2.0'),
'skipped': False,
},
]
FAILED tests/installation/test_installer.py::test_run_with_conflicting_dependency_extras[extra-one-False] - poetry.puzzle.exceptions.SolverProblemError: Because demo[demo-extra-two] (1.0.0) depends on transitive-dep (1.2.0)
and demo[demo-extra-one] (1.0.0) depends on transitive-dep (1.1.0), demo[demo-extra-two] (1.0.0) is incompatible with demo[demo-extra-one] (1.0.0).
So, because root depends on both demo[demo-extra-one] (1.0.0) and demo[demo-extra-two] (1.0.0), version solving failed.
FAILED tests/installation/test_installer.py::test_run_with_conflicting_dependency_extras[None-False] - poetry.puzzle.exceptions.SolverProblemError: Because demo[demo-extra-two] (1.0.0) depends on transitive-dep (1.2.0)
and demo[demo-extra-one] (1.0.0) depends on transitive-dep (1.1.0), demo[demo-extra-two] (1.0.0) is incompatible with demo[demo-extra-one] (1.0.0).
So, because root depends on both demo[demo-extra-one] (1.0.0) and demo[demo-extra-two] (1.0.0), version solving failed.
FAILED tests/installation/test_installer.py::test_run_with_conflicting_dependency_extras[extra-two-False] - poetry.puzzle.exceptions.SolverProblemError: Because demo[demo-extra-two] (1.0.0) depends on transitive-dep (1.2.0)
and demo[demo-extra-one] (1.0.0) depends on transitive-dep (1.1.0), demo[demo-extra-two] (1.0.0) is incompatible with demo[demo-extra-one] (1.0.0).
So, because root depends on both demo[demo-extra-one] (1.0.0) and demo[demo-extra-two] (1.0.0), version solving failed.
============= 5 failed, 1706 passed, 7 skipped in 81.50s (0:01:21) =============
Annotations
Check failure on line 4741 in tests/puzzle/test_solver.py
cirrus-ci / Tests / FreeBSD (Python 3.8) / pytest
tests/puzzle/test_solver.py#L4741
tests.puzzle.test_solver.test_solver_resolves_duplicate_dependency_in_root_extra[True]
Raw output
package = Package('root', '1.0')
pool = <poetry.repositories.repository_pool.RepositoryPool object at 0x180045d71130>
repo = <poetry.repositories.repository.Repository object at 0x180045d710d0>
io = <cleo.io.null_io.NullIO object at 0x18003bb700a0>, with_extra = True
@pytest.mark.parametrize("with_extra", [False, True])
def test_solver_resolves_duplicate_dependency_in_root_extra(
package: ProjectPackage,
pool: RepositoryPool,
repo: Repository,
io: NullIO,
with_extra: bool,
) -> None:
"""
Without extras, a newer version of A can be chosen than with root extras.
"""
constraint: dict[str, Any] = {"version": "*"}
if with_extra:
constraint["extras"] = ["foo"]
package_a1 = get_package("A", "1.0")
package_a2 = get_package("A", "2.0")
dep = get_dependency("A", ">=1.0")
package.add_dependency(dep)
dep_extra = get_dependency("A", "^1.0", optional=True)
dep_extra.marker = parse_marker("extra == 'foo'")
package.extras = {canonicalize_name("foo"): [dep_extra]}
package.add_dependency(dep_extra)
repo.add_package(package_a1)
repo.add_package(package_a2)
solver = Solver(package, pool, [], [], io)
transaction = solver.solve()
> check_solver_result(
transaction,
(
[
{"job": "install", "package": package_a1 if with_extra else package_a2},
]
),
)
/tmp/cirrus-ci-build/tests/puzzle/test_solver.py:4741:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
transaction = <poetry.puzzle.transaction.Transaction object at 0x180045d234c0>
expected = [{'job': 'install', 'package': Package('a', '1.0'), 'skipped': False}]
synchronize = False
def check_solver_result(
transaction: Transaction,
expected: list[dict[str, Any]],
synchronize: bool = False,
) -> list[Operation]:
for e in expected:
if "skipped" not in e:
e["skipped"] = False
result = []
ops = transaction.calculate_operations(synchronize=synchronize)
for op in ops:
if op.job_type == "update":
assert isinstance(op, Update)
result.append(
{
"job": "update",
"from": op.initial_package,
"to": op.target_package,
"skipped": op.skipped,
}
)
else:
job = "install"
if op.job_type == "uninstall":
job = "remove"
result.append({"job": job, "package": op.package, "skipped": op.skipped})
> assert result == expected
E AssertionError: assert [{'job': 'ins...pped': False}] == [{'job': 'ins...pped': False}]
E
E Left contains one more item: {'job': 'install', 'package': Package('a', '2.0'), 'skipped': False}
E
E Full diff:
E [
E {
E 'job': 'install',
E 'package': Package('a', '1.0'),
E 'skipped': False,
E },
E + {
E + 'job': 'install',
E + 'package': Package('a', '2.0'),
E + 'skipped': False,
E + },
E ]
/tmp/cirrus-ci-build/tests/puzzle/test_solver.py:113: AssertionError
Check failure on line 4741 in tests/puzzle/test_solver.py
cirrus-ci / Tests / FreeBSD (Python 3.8) / pytest
tests/puzzle/test_solver.py#L4741
tests.puzzle.test_solver.test_solver_resolves_duplicate_dependency_in_root_extra[False]
Raw output
package = Package('root', '1.0')
pool = <poetry.repositories.repository_pool.RepositoryPool object at 0x180045dfe1c0>
repo = <poetry.repositories.repository.Repository object at 0x180045dfea00>
io = <cleo.io.null_io.NullIO object at 0x18003ad76c10>, with_extra = False
@pytest.mark.parametrize("with_extra", [False, True])
def test_solver_resolves_duplicate_dependency_in_root_extra(
package: ProjectPackage,
pool: RepositoryPool,
repo: Repository,
io: NullIO,
with_extra: bool,
) -> None:
"""
Without extras, a newer version of A can be chosen than with root extras.
"""
constraint: dict[str, Any] = {"version": "*"}
if with_extra:
constraint["extras"] = ["foo"]
package_a1 = get_package("A", "1.0")
package_a2 = get_package("A", "2.0")
dep = get_dependency("A", ">=1.0")
package.add_dependency(dep)
dep_extra = get_dependency("A", "^1.0", optional=True)
dep_extra.marker = parse_marker("extra == 'foo'")
package.extras = {canonicalize_name("foo"): [dep_extra]}
package.add_dependency(dep_extra)
repo.add_package(package_a1)
repo.add_package(package_a2)
solver = Solver(package, pool, [], [], io)
transaction = solver.solve()
> check_solver_result(
transaction,
(
[
{"job": "install", "package": package_a1 if with_extra else package_a2},
]
),
)
/tmp/cirrus-ci-build/tests/puzzle/test_solver.py:4741:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
transaction = <poetry.puzzle.transaction.Transaction object at 0x18003ad76f40>
expected = [{'job': 'install', 'package': Package('a', '2.0'), 'skipped': False}]
synchronize = False
def check_solver_result(
transaction: Transaction,
expected: list[dict[str, Any]],
synchronize: bool = False,
) -> list[Operation]:
for e in expected:
if "skipped" not in e:
e["skipped"] = False
result = []
ops = transaction.calculate_operations(synchronize=synchronize)
for op in ops:
if op.job_type == "update":
assert isinstance(op, Update)
result.append(
{
"job": "update",
"from": op.initial_package,
"to": op.target_package,
"skipped": op.skipped,
}
)
else:
job = "install"
if op.job_type == "uninstall":
job = "remove"
result.append({"job": job, "package": op.package, "skipped": op.skipped})
> assert result == expected
E AssertionError: assert [{'job': 'ins...pped': False}] == [{'job': 'ins...pped': False}]
E
E At index 0 diff: {'job': 'install', 'package': Package('a', '1.0'), 'skipped': False} != {'job': 'install', 'package': Package('a', '2.0'), 'skipped': False}
E Left contains one more item: {'job': 'install', 'package': Package('a', '2.0'), 'skipped': False}
E
E Full diff:
E [
E + {
E + 'job': 'install',
E + 'package': Package('a', '1.0'),
E + 'skipped': False,
E + },
E {
E 'job': 'install',
E 'package': Package('a', '2.0'),
E 'skipped': False,
E },
E ]
/tmp/cirrus-ci-build/tests/puzzle/test_solver.py:113: AssertionError
Check failure on line 161 in src/poetry/puzzle/solver.py
cirrus-ci / Tests / FreeBSD (Python 3.8) / pytest
src/poetry/puzzle/solver.py#L161
tests.installation.test_installer.test_run_with_conflicting_dependency_extras[extra-one-False]
Raw output
self = <poetry.puzzle.solver.Solver object at 0x23c87d586490>
def _solve(self) -> tuple[list[Package], list[int]]:
if self._provider._overrides:
self._overrides.append(self._provider._overrides)
try:
> result = resolve_version(self._package, self._provider)
/tmp/cirrus-ci-build/src/poetry/puzzle/solver.py:161:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
/tmp/cirrus-ci-build/src/poetry/mixology/__init__.py:18: in resolve_version
return solver.solve()
/tmp/cirrus-ci-build/src/poetry/mixology/version_solver.py:174: in solve
self._propagate(next)
/tmp/cirrus-ci-build/src/poetry/mixology/version_solver.py:213: in _propagate
root_cause = self._resolve_conflict(incompatibility)
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <poetry.mixology.version_solver.VersionSolver object at 0x23c87d586700>
incompatibility = <Incompatibility version solving failed>
def _resolve_conflict(self, incompatibility: Incompatibility) -> Incompatibility:
"""
Given an incompatibility that's satisfied by _solution,
The `conflict resolution`_ constructs a new incompatibility that encapsulates
the root cause of the conflict and backtracks _solution until the new
incompatibility will allow _propagate() to deduce new assignments.
Adds the new incompatibility to _incompatibilities and returns it.
.. _conflict resolution:
https://github.com/dart-lang/pub/tree/master/doc/solver.md#conflict-resolution
"""
self._log(f"conflict: {incompatibility}")
new_incompatibility = False
while not incompatibility.is_failure():
# The term in incompatibility.terms that was most recently satisfied by
# _solution.
most_recent_term = None
# The earliest assignment in _solution such that incompatibility is
# satisfied by _solution up to and including this assignment.
most_recent_satisfier = None
# The difference between most_recent_satisfier and most_recent_term;
# that is, the versions that are allowed by most_recent_satisfier and not
# by most_recent_term. This is None if most_recent_satisfier totally
# satisfies most_recent_term.
difference = None
# The decision level of the earliest assignment in _solution *before*
# most_recent_satisfier such that incompatibility is satisfied by
# _solution up to and including this assignment plus
# most_recent_satisfier.
#
# Decision level 1 is the level where the root package was selected. It's
# safe to go back to decision level 0, but stopping at 1 tends to produce
# better error messages, because references to the root package end up
# closer to the final conclusion that no solution exists.
previous_satisfier_level = 1
for term in incompatibility.terms:
satisfier = self._solution.satisfier(term)
if most_recent_satisfier is None:
most_recent_term = term
most_recent_satisfier = satisfier
elif most_recent_satisfier.index < satisfier.index:
previous_satisfier_level = max(
previous_satisfier_level, most_recent_satisfier.decision_level
)
most_recent_term = term
most_recent_satisfier = satisfier
difference = None
else:
previous_satisfier_level = max(
previous_satisfier_level, satisfier.decision_level
)
if most_recent_term == term:
# If most_recent_satisfier doesn't satisfy most_recent_term on its
# own, then the next-most-recent satisfier may be the one that
# satisfies the remainder.
difference = most_recent_satisfier.difference(most_recent_term)
if difference is not None:
previous_satisfier_level = max(
previous_satisfier_level,
self._solution.satisfier(difference.inverse).decision_level,
)
# If most_recent_identifier is the only satisfier left at its decision
# level, or if it has no cause (indicating that it's a decision rather
# than a derivation), then incompatibility is the root cause. We then
# backjump to previous_satisfier_level, where incompatibility is
# guaranteed to allow _propagate to produce more assignments.
# using assert to suppress mypy [union-attr]
assert most_recent_satisfier is not None
if (
previous_satisfier_level < most_recent_satisfier.decision_level
or most_recent_satisfier.cause is None
):
for level in range(
self._solution.decision_level, previous_satisfier_level, -1
):
if level in self._contradicted_incompatibilities_by_level:
self._contradicted_incompatibilities.difference_update(
self._contradicted_incompatibilities_by_level.pop(level),
)
self._dependency_cache.clear_level(level)
self._solution.backtrack(previous_satisfier_level)
if new_incompatibility:
self._add_incompatibility(incompatibility)
return incompatibility
# Create a new incompatibility by combining incompatibility with the
# incompatibility that caused most_recent_satisfier to be assigned. Doing
# this iteratively constructs an incompatibility that's guaranteed to be
# true (that is, we know for sure no solution will satisfy the
# incompatibility) while also approximating the intuitive notion of the
# "root cause" of the conflict.
new_terms = [
term for term in incompatibility.terms if term != most_recent_term
]
for term in most_recent_satisfier.cause.terms:
if term.dependency != most_recent_satisfier.dependency:
new_terms.append(term)
# The most_recent_satisfier may not satisfy most_recent_term on its own
# if there are a collection of constraints on most_recent_term that
# only satisfy it together. For example, if most_recent_term is
# `foo ^1.0.0` and _solution contains `[foo >=1.0.0,
# foo <2.0.0]`, then most_recent_satisfier will be `foo <2.0.0` even
# though it doesn't totally satisfy `foo ^1.0.0`.
#
# In this case, we add `not (most_recent_satisfier \ most_recent_term)` to
# the incompatibility as well, See the `algorithm documentation`_ for
# details.
#
# .. _algorithm documentation:
# https://github.com/dart-lang/pub/tree/master/doc/solver.md#conflict-resolution
if difference is not None:
inverse = difference.inverse
if inverse.dependency != most_recent_satisfier.dependency:
new_terms.append(inverse)
incompatibility = Incompatibility(
new_terms, ConflictCause(incompatibility, most_recent_satisfier.cause)
)
new_incompatibility = True
partially = "" if difference is None else " partially"
self._log(
f"! {most_recent_term} is{partially} satisfied by"
f" {most_recent_satisfier}"
)
self._log(f'! which is caused by "{most_recent_satisfier.cause}"')
self._log(f"! thus: {incompatibility}")
> raise SolveFailure(incompatibility)
E poetry.mixology.failure.SolveFailure: Because demo[demo-extra-two] (1.0.0) depends on transitive-dep (1.2.0)
E and demo[demo-extra-one] (1.0.0) depends on transitive-dep (1.1.0), demo[demo-extra-two] (1.0.0) is incompatible with demo[demo-extra-one] (1.0.0).
E So, because root depends on both demo[demo-extra-one] (1.0.0) and demo[demo-extra-two] (1.0.0), version solving failed.
/tmp/cirrus-ci-build/src/poetry/mixology/version_solver.py:427: SolveFailure
During handling of the above exception, another exception occurred:
installer = <poetry.installation.installer.Installer object at 0x23c888b1e550>
pool = <poetry.repositories.repository_pool.RepositoryPool object at 0x23c8886cbd00>
locker = <tests.installation.test_installer.Locker object at 0x23c87d87d280>
installed = <tests.installation.test_installer.CustomInstalledRepository object at 0x23c87d87d6a0>
repo = <poetry.repositories.repository.Repository object at 0x23c8886cb1c0>
config = <tests.conftest.Config object at 0x23c88896c850>
package = Package('root', '1.0'), extra = 'extra-one', locked = False
@pytest.mark.parametrize("locked", [False]) # TODO: lock data
@pytest.mark.parametrize("extra", [None, "extra-one", "extra-two"])
def test_run_with_conflicting_dependency_extras(
installer: Installer,
pool: RepositoryPool,
locker: Locker,
installed: CustomInstalledRepository,
repo: Repository,
config: Config,
package: ProjectPackage,
extra: str | None,
locked: bool,
) -> None:
"""https://github.com/python-poetry/poetry/issues/834
Tests resolution of extras in both root ('extra-one', 'extra-two') and transitive
('demo-extra-one', 'demo-extra-two') dependencies
"""
# Demo package with two optional transitive dependencies, one for each extra
demo_pkg = get_package("demo", "1.0.0")
transitive_dep_one = get_package("transitive-dep", "1.1.0")
transitive_dep_two = get_package("transitive-dep", "1.2.0")
demo_pkg.extras = {
canonicalize_name("demo-extra-one"): [
get_dependency("transitive-dep", constraint={"version": "1.1.0", "optional": True})
],
canonicalize_name("demo-extra-two"): [
get_dependency("transitive-dep", constraint={"version": "1.2.0", "optional": True})
],
}
demo_pkg.add_dependency(
Factory.create_dependency(
"transitive-dep",
{
"version": "1.1.0",
"markers": "extra == 'demo-extra-one' and extra != 'demo-extra-two'",
"optional": True,
}
)
)
demo_pkg.add_dependency(
Factory.create_dependency(
"transitive-dep",
{
"version": "1.2.0",
"markers": "extra != 'demo-extra-one' and extra == 'demo-extra-two'",
"optional": True,
}
)
)
repo.add_package(demo_pkg)
repo.add_package(transitive_dep_one)
repo.add_package(transitive_dep_two)
# 'demo' with extra 'demo-extra-one' when package has 'extra-one' extra
# and with extra 'demo-extra-two' when 'extra-two'
extra_one_dep = Factory.create_dependency(
"demo",
{
"version": "1.0.0",
"markers": "extra == 'extra-one' and extra != 'extra-two'",
"extras": ["demo-extra-one"],
"optional": True,
},
)
extra_two_dep = Factory.create_dependency(
"demo",
{
"version": "1.0.0",
"markers": "extra != 'extra-one' and extra == 'extra-two'",
"extras": ["demo-extra-two"],
"optional": True,
},
)
package.add_dependency(extra_one_dep)
package.add_dependency(extra_two_dep)
package.extras = {
canonicalize_name("extra-one"): [extra_one_dep],
canonicalize_name("extra-two"): [extra_two_dep],
}
locker.locked(locked)
if locked:
raise ValueError("no lock data for this test yet")
locker.mock_lock_data(dict(fixture("TODO")))
if extra is not None:
installer.extras([extra])
> result = installer.run()
/tmp/cirrus-ci-build/tests/installation/test_installer.py:1118:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
/tmp/cirrus-ci-build/src/poetry/installation/installer.py:102: in run
return self._do_install()
/tmp/cirrus-ci-build/src/poetry/installation/installer.py:239: in _do_install
ops = solver.solve(use_latest=self._whitelist).calculate_operations()
/tmp/cirrus-ci-build/src/poetry/puzzle/solver.py:78: in solve
packages, depths = self._solve()
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <poetry.puzzle.solver.Solver object at 0x23c87d586490>
def _solve(self) -> tuple[list[Package], list[int]]:
if self._provider._overrides:
self._overrides.append(self._provider._overrides)
try:
result = resolve_version(self._package, self._provider)
packages = result.packages
except OverrideNeeded as e:
return self._solve_in_compatibility_mode(e.overrides)
except SolveFailure as e:
> raise SolverProblemError(e)
E poetry.puzzle.exceptions.SolverProblemError: Because demo[demo-extra-two] (1.0.0) depends on transitive-dep (1.2.0)
E and demo[demo-extra-one] (1.0.0) depends on transitive-dep (1.1.0), demo[demo-extra-two] (1.0.0) is incompatible with demo[demo-extra-one] (1.0.0).
E So, because root depends on both demo[demo-extra-one] (1.0.0) and demo[demo-extra-two] (1.0.0), version solving failed.
/tmp/cirrus-ci-build/src/poetry/puzzle/solver.py:167: SolverProblemError
Check failure on line 161 in src/poetry/puzzle/solver.py
cirrus-ci / Tests / FreeBSD (Python 3.8) / pytest
src/poetry/puzzle/solver.py#L161
tests.installation.test_installer.test_run_with_conflicting_dependency_extras[None-False]
Raw output
self = <poetry.puzzle.solver.Solver object at 0x18004787f760>
def _solve(self) -> tuple[list[Package], list[int]]:
if self._provider._overrides:
self._overrides.append(self._provider._overrides)
try:
> result = resolve_version(self._package, self._provider)
/tmp/cirrus-ci-build/src/poetry/puzzle/solver.py:161:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
/tmp/cirrus-ci-build/src/poetry/mixology/__init__.py:18: in resolve_version
return solver.solve()
/tmp/cirrus-ci-build/src/poetry/mixology/version_solver.py:174: in solve
self._propagate(next)
/tmp/cirrus-ci-build/src/poetry/mixology/version_solver.py:213: in _propagate
root_cause = self._resolve_conflict(incompatibility)
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <poetry.mixology.version_solver.VersionSolver object at 0x180045b5c1c0>
incompatibility = <Incompatibility version solving failed>
def _resolve_conflict(self, incompatibility: Incompatibility) -> Incompatibility:
"""
Given an incompatibility that's satisfied by _solution,
The `conflict resolution`_ constructs a new incompatibility that encapsulates
the root cause of the conflict and backtracks _solution until the new
incompatibility will allow _propagate() to deduce new assignments.
Adds the new incompatibility to _incompatibilities and returns it.
.. _conflict resolution:
https://github.com/dart-lang/pub/tree/master/doc/solver.md#conflict-resolution
"""
self._log(f"conflict: {incompatibility}")
new_incompatibility = False
while not incompatibility.is_failure():
# The term in incompatibility.terms that was most recently satisfied by
# _solution.
most_recent_term = None
# The earliest assignment in _solution such that incompatibility is
# satisfied by _solution up to and including this assignment.
most_recent_satisfier = None
# The difference between most_recent_satisfier and most_recent_term;
# that is, the versions that are allowed by most_recent_satisfier and not
# by most_recent_term. This is None if most_recent_satisfier totally
# satisfies most_recent_term.
difference = None
# The decision level of the earliest assignment in _solution *before*
# most_recent_satisfier such that incompatibility is satisfied by
# _solution up to and including this assignment plus
# most_recent_satisfier.
#
# Decision level 1 is the level where the root package was selected. It's
# safe to go back to decision level 0, but stopping at 1 tends to produce
# better error messages, because references to the root package end up
# closer to the final conclusion that no solution exists.
previous_satisfier_level = 1
for term in incompatibility.terms:
satisfier = self._solution.satisfier(term)
if most_recent_satisfier is None:
most_recent_term = term
most_recent_satisfier = satisfier
elif most_recent_satisfier.index < satisfier.index:
previous_satisfier_level = max(
previous_satisfier_level, most_recent_satisfier.decision_level
)
most_recent_term = term
most_recent_satisfier = satisfier
difference = None
else:
previous_satisfier_level = max(
previous_satisfier_level, satisfier.decision_level
)
if most_recent_term == term:
# If most_recent_satisfier doesn't satisfy most_recent_term on its
# own, then the next-most-recent satisfier may be the one that
# satisfies the remainder.
difference = most_recent_satisfier.difference(most_recent_term)
if difference is not None:
previous_satisfier_level = max(
previous_satisfier_level,
self._solution.satisfier(difference.inverse).decision_level,
)
# If most_recent_identifier is the only satisfier left at its decision
# level, or if it has no cause (indicating that it's a decision rather
# than a derivation), then incompatibility is the root cause. We then
# backjump to previous_satisfier_level, where incompatibility is
# guaranteed to allow _propagate to produce more assignments.
# using assert to suppress mypy [union-attr]
assert most_recent_satisfier is not None
if (
previous_satisfier_level < most_recent_satisfier.decision_level
or most_recent_satisfier.cause is None
):
for level in range(
self._solution.decision_level, previous_satisfier_level, -1
):
if level in self._contradicted_incompatibilities_by_level:
self._contradicted_incompatibilities.difference_update(
self._contradicted_incompatibilities_by_level.pop(level),
)
self._dependency_cache.clear_level(level)
self._solution.backtrack(previous_satisfier_level)
if new_incompatibility:
self._add_incompatibility(incompatibility)
return incompatibility
# Create a new incompatibility by combining incompatibility with the
# incompatibility that caused most_recent_satisfier to be assigned. Doing
# this iteratively constructs an incompatibility that's guaranteed to be
# true (that is, we know for sure no solution will satisfy the
# incompatibility) while also approximating the intuitive notion of the
# "root cause" of the conflict.
new_terms = [
term for term in incompatibility.terms if term != most_recent_term
]
for term in most_recent_satisfier.cause.terms:
if term.dependency != most_recent_satisfier.dependency:
new_terms.append(term)
# The most_recent_satisfier may not satisfy most_recent_term on its own
# if there are a collection of constraints on most_recent_term that
# only satisfy it together. For example, if most_recent_term is
# `foo ^1.0.0` and _solution contains `[foo >=1.0.0,
# foo <2.0.0]`, then most_recent_satisfier will be `foo <2.0.0` even
# though it doesn't totally satisfy `foo ^1.0.0`.
#
# In this case, we add `not (most_recent_satisfier \ most_recent_term)` to
# the incompatibility as well, See the `algorithm documentation`_ for
# details.
#
# .. _algorithm documentation:
# https://github.com/dart-lang/pub/tree/master/doc/solver.md#conflict-resolution
if difference is not None:
inverse = difference.inverse
if inverse.dependency != most_recent_satisfier.dependency:
new_terms.append(inverse)
incompatibility = Incompatibility(
new_terms, ConflictCause(incompatibility, most_recent_satisfier.cause)
)
new_incompatibility = True
partially = "" if difference is None else " partially"
self._log(
f"! {most_recent_term} is{partially} satisfied by"
f" {most_recent_satisfier}"
)
self._log(f'! which is caused by "{most_recent_satisfier.cause}"')
self._log(f"! thus: {incompatibility}")
> raise SolveFailure(incompatibility)
E poetry.mixology.failure.SolveFailure: Because demo[demo-extra-two] (1.0.0) depends on transitive-dep (1.2.0)
E and demo[demo-extra-one] (1.0.0) depends on transitive-dep (1.1.0), demo[demo-extra-two] (1.0.0) is incompatible with demo[demo-extra-one] (1.0.0).
E So, because root depends on both demo[demo-extra-one] (1.0.0) and demo[demo-extra-two] (1.0.0), version solving failed.
/tmp/cirrus-ci-build/src/poetry/mixology/version_solver.py:427: SolveFailure
During handling of the above exception, another exception occurred:
installer = <poetry.installation.installer.Installer object at 0x18004787f880>
pool = <poetry.repositories.repository_pool.RepositoryPool object at 0x180046486d60>
locker = <tests.installation.test_installer.Locker object at 0x180046486700>
installed = <tests.installation.test_installer.CustomInstalledRepository object at 0x180046486fd0>
repo = <poetry.repositories.repository.Repository object at 0x1800464868e0>
config = <tests.conftest.Config object at 0x180047bac490>
package = Package('root', '1.0'), extra = None, locked = False
@pytest.mark.parametrize("locked", [False]) # TODO: lock data
@pytest.mark.parametrize("extra", [None, "extra-one", "extra-two"])
def test_run_with_conflicting_dependency_extras(
installer: Installer,
pool: RepositoryPool,
locker: Locker,
installed: CustomInstalledRepository,
repo: Repository,
config: Config,
package: ProjectPackage,
extra: str | None,
locked: bool,
) -> None:
"""https://github.com/python-poetry/poetry/issues/834
Tests resolution of extras in both root ('extra-one', 'extra-two') and transitive
('demo-extra-one', 'demo-extra-two') dependencies
"""
# Demo package with two optional transitive dependencies, one for each extra
demo_pkg = get_package("demo", "1.0.0")
transitive_dep_one = get_package("transitive-dep", "1.1.0")
transitive_dep_two = get_package("transitive-dep", "1.2.0")
demo_pkg.extras = {
canonicalize_name("demo-extra-one"): [
get_dependency("transitive-dep", constraint={"version": "1.1.0", "optional": True})
],
canonicalize_name("demo-extra-two"): [
get_dependency("transitive-dep", constraint={"version": "1.2.0", "optional": True})
],
}
demo_pkg.add_dependency(
Factory.create_dependency(
"transitive-dep",
{
"version": "1.1.0",
"markers": "extra == 'demo-extra-one' and extra != 'demo-extra-two'",
"optional": True,
}
)
)
demo_pkg.add_dependency(
Factory.create_dependency(
"transitive-dep",
{
"version": "1.2.0",
"markers": "extra != 'demo-extra-one' and extra == 'demo-extra-two'",
"optional": True,
}
)
)
repo.add_package(demo_pkg)
repo.add_package(transitive_dep_one)
repo.add_package(transitive_dep_two)
# 'demo' with extra 'demo-extra-one' when package has 'extra-one' extra
# and with extra 'demo-extra-two' when 'extra-two'
extra_one_dep = Factory.create_dependency(
"demo",
{
"version": "1.0.0",
"markers": "extra == 'extra-one' and extra != 'extra-two'",
"extras": ["demo-extra-one"],
"optional": True,
},
)
extra_two_dep = Factory.create_dependency(
"demo",
{
"version": "1.0.0",
"markers": "extra != 'extra-one' and extra == 'extra-two'",
"extras": ["demo-extra-two"],
"optional": True,
},
)
package.add_dependency(extra_one_dep)
package.add_dependency(extra_two_dep)
package.extras = {
canonicalize_name("extra-one"): [extra_one_dep],
canonicalize_name("extra-two"): [extra_two_dep],
}
locker.locked(locked)
if locked:
raise ValueError("no lock data for this test yet")
locker.mock_lock_data(dict(fixture("TODO")))
if extra is not None:
installer.extras([extra])
> result = installer.run()
/tmp/cirrus-ci-build/tests/installation/test_installer.py:1118:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
/tmp/cirrus-ci-build/src/poetry/installation/installer.py:102: in run
return self._do_install()
/tmp/cirrus-ci-build/src/poetry/installation/installer.py:239: in _do_install
ops = solver.solve(use_latest=self._whitelist).calculate_operations()
/tmp/cirrus-ci-build/src/poetry/puzzle/solver.py:78: in solve
packages, depths = self._solve()
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <poetry.puzzle.solver.Solver object at 0x18004787f760>
def _solve(self) -> tuple[list[Package], list[int]]:
if self._provider._overrides:
self._overrides.append(self._provider._overrides)
try:
result = resolve_version(self._package, self._provider)
packages = result.packages
except OverrideNeeded as e:
return self._solve_in_compatibility_mode(e.overrides)
except SolveFailure as e:
> raise SolverProblemError(e)
E poetry.puzzle.exceptions.SolverProblemError: Because demo[demo-extra-two] (1.0.0) depends on transitive-dep (1.2.0)
E and demo[demo-extra-one] (1.0.0) depends on transitive-dep (1.1.0), demo[demo-extra-two] (1.0.0) is incompatible with demo[demo-extra-one] (1.0.0).
E So, because root depends on both demo[demo-extra-one] (1.0.0) and demo[demo-extra-two] (1.0.0), version solving failed.
/tmp/cirrus-ci-build/src/poetry/puzzle/solver.py:167: SolverProblemError
Check failure on line 161 in src/poetry/puzzle/solver.py
cirrus-ci / Tests / FreeBSD (Python 3.8) / pytest
src/poetry/puzzle/solver.py#L161
tests.installation.test_installer.test_run_with_conflicting_dependency_extras[extra-two-False]
Raw output
self = <poetry.puzzle.solver.Solver object at 0x23c8887caee0>
def _solve(self) -> tuple[list[Package], list[int]]:
if self._provider._overrides:
self._overrides.append(self._provider._overrides)
try:
> result = resolve_version(self._package, self._provider)
/tmp/cirrus-ci-build/src/poetry/puzzle/solver.py:161:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
/tmp/cirrus-ci-build/src/poetry/mixology/__init__.py:18: in resolve_version
return solver.solve()
/tmp/cirrus-ci-build/src/poetry/mixology/version_solver.py:174: in solve
self._propagate(next)
/tmp/cirrus-ci-build/src/poetry/mixology/version_solver.py:213: in _propagate
root_cause = self._resolve_conflict(incompatibility)
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <poetry.mixology.version_solver.VersionSolver object at 0x23c8887cabe0>
incompatibility = <Incompatibility version solving failed>
def _resolve_conflict(self, incompatibility: Incompatibility) -> Incompatibility:
"""
Given an incompatibility that's satisfied by _solution,
The `conflict resolution`_ constructs a new incompatibility that encapsulates
the root cause of the conflict and backtracks _solution until the new
incompatibility will allow _propagate() to deduce new assignments.
Adds the new incompatibility to _incompatibilities and returns it.
.. _conflict resolution:
https://github.com/dart-lang/pub/tree/master/doc/solver.md#conflict-resolution
"""
self._log(f"conflict: {incompatibility}")
new_incompatibility = False
while not incompatibility.is_failure():
# The term in incompatibility.terms that was most recently satisfied by
# _solution.
most_recent_term = None
# The earliest assignment in _solution such that incompatibility is
# satisfied by _solution up to and including this assignment.
most_recent_satisfier = None
# The difference between most_recent_satisfier and most_recent_term;
# that is, the versions that are allowed by most_recent_satisfier and not
# by most_recent_term. This is None if most_recent_satisfier totally
# satisfies most_recent_term.
difference = None
# The decision level of the earliest assignment in _solution *before*
# most_recent_satisfier such that incompatibility is satisfied by
# _solution up to and including this assignment plus
# most_recent_satisfier.
#
# Decision level 1 is the level where the root package was selected. It's
# safe to go back to decision level 0, but stopping at 1 tends to produce
# better error messages, because references to the root package end up
# closer to the final conclusion that no solution exists.
previous_satisfier_level = 1
for term in incompatibility.terms:
satisfier = self._solution.satisfier(term)
if most_recent_satisfier is None:
most_recent_term = term
most_recent_satisfier = satisfier
elif most_recent_satisfier.index < satisfier.index:
previous_satisfier_level = max(
previous_satisfier_level, most_recent_satisfier.decision_level
)
most_recent_term = term
most_recent_satisfier = satisfier
difference = None
else:
previous_satisfier_level = max(
previous_satisfier_level, satisfier.decision_level
)
if most_recent_term == term:
# If most_recent_satisfier doesn't satisfy most_recent_term on its
# own, then the next-most-recent satisfier may be the one that
# satisfies the remainder.
difference = most_recent_satisfier.difference(most_recent_term)
if difference is not None:
previous_satisfier_level = max(
previous_satisfier_level,
self._solution.satisfier(difference.inverse).decision_level,
)
# If most_recent_identifier is the only satisfier left at its decision
# level, or if it has no cause (indicating that it's a decision rather
# than a derivation), then incompatibility is the root cause. We then
# backjump to previous_satisfier_level, where incompatibility is
# guaranteed to allow _propagate to produce more assignments.
# using assert to suppress mypy [union-attr]
assert most_recent_satisfier is not None
if (
previous_satisfier_level < most_recent_satisfier.decision_level
or most_recent_satisfier.cause is None
):
for level in range(
self._solution.decision_level, previous_satisfier_level, -1
):
if level in self._contradicted_incompatibilities_by_level:
self._contradicted_incompatibilities.difference_update(
self._contradicted_incompatibilities_by_level.pop(level),
)
self._dependency_cache.clear_level(level)
self._solution.backtrack(previous_satisfier_level)
if new_incompatibility:
self._add_incompatibility(incompatibility)
return incompatibility
# Create a new incompatibility by combining incompatibility with the
# incompatibility that caused most_recent_satisfier to be assigned. Doing
# this iteratively constructs an incompatibility that's guaranteed to be
# true (that is, we know for sure no solution will satisfy the
# incompatibility) while also approximating the intuitive notion of the
# "root cause" of the conflict.
new_terms = [
term for term in incompatibility.terms if term != most_recent_term
]
for term in most_recent_satisfier.cause.terms:
if term.dependency != most_recent_satisfier.dependency:
new_terms.append(term)
# The most_recent_satisfier may not satisfy most_recent_term on its own
# if there are a collection of constraints on most_recent_term that
# only satisfy it together. For example, if most_recent_term is
# `foo ^1.0.0` and _solution contains `[foo >=1.0.0,
# foo <2.0.0]`, then most_recent_satisfier will be `foo <2.0.0` even
# though it doesn't totally satisfy `foo ^1.0.0`.
#
# In this case, we add `not (most_recent_satisfier \ most_recent_term)` to
# the incompatibility as well, See the `algorithm documentation`_ for
# details.
#
# .. _algorithm documentation:
# https://github.com/dart-lang/pub/tree/master/doc/solver.md#conflict-resolution
if difference is not None:
inverse = difference.inverse
if inverse.dependency != most_recent_satisfier.dependency:
new_terms.append(inverse)
incompatibility = Incompatibility(
new_terms, ConflictCause(incompatibility, most_recent_satisfier.cause)
)
new_incompatibility = True
partially = "" if difference is None else " partially"
self._log(
f"! {most_recent_term} is{partially} satisfied by"
f" {most_recent_satisfier}"
)
self._log(f'! which is caused by "{most_recent_satisfier.cause}"')
self._log(f"! thus: {incompatibility}")
> raise SolveFailure(incompatibility)
E poetry.mixology.failure.SolveFailure: Because demo[demo-extra-two] (1.0.0) depends on transitive-dep (1.2.0)
E and demo[demo-extra-one] (1.0.0) depends on transitive-dep (1.1.0), demo[demo-extra-two] (1.0.0) is incompatible with demo[demo-extra-one] (1.0.0).
E So, because root depends on both demo[demo-extra-one] (1.0.0) and demo[demo-extra-two] (1.0.0), version solving failed.
/tmp/cirrus-ci-build/src/poetry/mixology/version_solver.py:427: SolveFailure
During handling of the above exception, another exception occurred:
installer = <poetry.installation.installer.Installer object at 0x23c884176550>
pool = <poetry.repositories.repository_pool.RepositoryPool object at 0x23c88a799040>
locker = <tests.installation.test_installer.Locker object at 0x23c88a799f10>
installed = <tests.installation.test_installer.CustomInstalledRepository object at 0x23c88a799670>
repo = <poetry.repositories.repository.Repository object at 0x23c88a7995b0>
config = <tests.conftest.Config object at 0x23c88a79bb50>
package = Package('root', '1.0'), extra = 'extra-two', locked = False
@pytest.mark.parametrize("locked", [False]) # TODO: lock data
@pytest.mark.parametrize("extra", [None, "extra-one", "extra-two"])
def test_run_with_conflicting_dependency_extras(
installer: Installer,
pool: RepositoryPool,
locker: Locker,
installed: CustomInstalledRepository,
repo: Repository,
config: Config,
package: ProjectPackage,
extra: str | None,
locked: bool,
) -> None:
"""https://github.com/python-poetry/poetry/issues/834
Tests resolution of extras in both root ('extra-one', 'extra-two') and transitive
('demo-extra-one', 'demo-extra-two') dependencies
"""
# Demo package with two optional transitive dependencies, one for each extra
demo_pkg = get_package("demo", "1.0.0")
transitive_dep_one = get_package("transitive-dep", "1.1.0")
transitive_dep_two = get_package("transitive-dep", "1.2.0")
demo_pkg.extras = {
canonicalize_name("demo-extra-one"): [
get_dependency("transitive-dep", constraint={"version": "1.1.0", "optional": True})
],
canonicalize_name("demo-extra-two"): [
get_dependency("transitive-dep", constraint={"version": "1.2.0", "optional": True})
],
}
demo_pkg.add_dependency(
Factory.create_dependency(
"transitive-dep",
{
"version": "1.1.0",
"markers": "extra == 'demo-extra-one' and extra != 'demo-extra-two'",
"optional": True,
}
)
)
demo_pkg.add_dependency(
Factory.create_dependency(
"transitive-dep",
{
"version": "1.2.0",
"markers": "extra != 'demo-extra-one' and extra == 'demo-extra-two'",
"optional": True,
}
)
)
repo.add_package(demo_pkg)
repo.add_package(transitive_dep_one)
repo.add_package(transitive_dep_two)
# 'demo' with extra 'demo-extra-one' when package has 'extra-one' extra
# and with extra 'demo-extra-two' when 'extra-two'
extra_one_dep = Factory.create_dependency(
"demo",
{
"version": "1.0.0",
"markers": "extra == 'extra-one' and extra != 'extra-two'",
"extras": ["demo-extra-one"],
"optional": True,
},
)
extra_two_dep = Factory.create_dependency(
"demo",
{
"version": "1.0.0",
"markers": "extra != 'extra-one' and extra == 'extra-two'",
"extras": ["demo-extra-two"],
"optional": True,
},
)
package.add_dependency(extra_one_dep)
package.add_dependency(extra_two_dep)
package.extras = {
canonicalize_name("extra-one"): [extra_one_dep],
canonicalize_name("extra-two"): [extra_two_dep],
}
locker.locked(locked)
if locked:
raise ValueError("no lock data for this test yet")
locker.mock_lock_data(dict(fixture("TODO")))
if extra is not None:
installer.extras([extra])
> result = installer.run()
/tmp/cirrus-ci-build/tests/installation/test_installer.py:1118:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
/tmp/cirrus-ci-build/src/poetry/installation/installer.py:102: in run
return self._do_install()
/tmp/cirrus-ci-build/src/poetry/installation/installer.py:239: in _do_install
ops = solver.solve(use_latest=self._whitelist).calculate_operations()
/tmp/cirrus-ci-build/src/poetry/puzzle/solver.py:78: in solve
packages, depths = self._solve()
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <poetry.puzzle.solver.Solver object at 0x23c8887caee0>
def _solve(self) -> tuple[list[Package], list[int]]:
if self._provider._overrides:
self._overrides.append(self._provider._overrides)
try:
result = resolve_version(self._package, self._provider)
packages = result.packages
except OverrideNeeded as e:
return self._solve_in_compatibility_mode(e.overrides)
except SolveFailure as e:
> raise SolverProblemError(e)
E poetry.puzzle.exceptions.SolverProblemError: Because demo[demo-extra-two] (1.0.0) depends on transitive-dep (1.2.0)
E and demo[demo-extra-one] (1.0.0) depends on transitive-dep (1.1.0), demo[demo-extra-two] (1.0.0) is incompatible with demo[demo-extra-one] (1.0.0).
E So, because root depends on both demo[demo-extra-one] (1.0.0) and demo[demo-extra-two] (1.0.0), version solving failed.
/tmp/cirrus-ci-build/src/poetry/puzzle/solver.py:167: SolverProblemError