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

Pass Install Extras to Markers #9553

Open
wants to merge 16 commits into
base: main
Choose a base branch
from
Open
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
47 changes: 47 additions & 0 deletions docs/dependency-specification.md
Original file line number Diff line number Diff line change
Expand Up @@ -300,6 +300,53 @@ via the `markers` property:
pathlib2 = { version = "^2.2", markers = "python_version <= '3.4' or sys_platform == 'win32'" }
```

### `extra` environment marker

Poetry populates the `extra` marker with each of the selected extras for the parent declaring the dependency. For
example, consider the following dependency in your root package:
```toml
[tool.poetry.dependencies]
pathlib2 = { version = "^2.2", markers = "extra == 'paths' and sys_platform == 'win32'", optional = true}
```
`pathlib2` will be installed when you install your package with `--extras paths` on a `win32` machine.
You'll also need to [define the `paths` extra in your project](./pyproject.md#extras).

#### Exclusive extras

Keep in mind that all combinations of possible extras available in your project need to be compatible with each other.
This means that in order to use differing or incompatible versions across different combinations, you need to make your
extra markers *exclusive*. For example, the following installs PyTorch from one source repository with CPU versions
when the `cuda` extra is *not* specified, while the other installs from another repository with a separate version set
for GPUs when the `cuda` extra *is* specified:

```toml
[tool.poetry.dependencies]
torch = [
{ markers = "extra != 'cuda'", version = "2.3.1+cpu", source = "pytorch-cpu", optional = true},
{ markers = "extra == 'cuda'", version = "2.3.1+cu118", source = "pytorch-cu118", optional = true},
]

[tool.poetry.extras]
cuda = ["torch"]

[[tool.poetry.source]]
name = "pytorch-cpu"
url = "https://download.pytorch.org/whl/cpu"
priority = "explicit"

[[tool.poetry.source]]
name = "pytorch-cu118"
url = "https://download.pytorch.org/whl/cu118"
priority = "explicit"
```

For the CPU case, we have to specify `"extra != 'cuda'"` because the version specified is not compatible with the
GPU (`cuda`) version.

This same logic applies when you want either-or extras. If a dependency for `extra-one` and
`extra-two` conflict, they will need to be restricted using `markers = "extra == 'extra-one' and extra != 'extra-two'"`
and vice versa.

## Multiple constraints dependencies

Sometimes, one of your dependency may have different version ranges depending
Expand Down
1 change: 1 addition & 0 deletions src/poetry/installation/installer.py
Original file line number Diff line number Diff line change
Expand Up @@ -287,6 +287,7 @@ def _do_install(self) -> int:
self._installed_repository.packages,
locked_repository.packages,
NullIO(),
active_root_extras=self._extras,
)
# Everything is resolved at this point, so we no longer need
# to load deferred dependencies (i.e. VCS, URL and path dependencies)
Expand Down
41 changes: 38 additions & 3 deletions src/poetry/puzzle/provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from collections import defaultdict
from contextlib import contextmanager
from typing import TYPE_CHECKING
from typing import Any
from typing import ClassVar
from typing import cast

Expand Down Expand Up @@ -117,6 +118,7 @@ def __init__(
*,
installed: list[Package] | None = None,
locked: list[Package] | None = None,
active_root_extras: Collection[NormalizedName] | None = None,
) -> None:
self._package = package
self._pool = pool
Expand All @@ -133,6 +135,9 @@ def __init__(
self._direct_origin_packages: dict[str, Package] = {}
self._locked: dict[NormalizedName, list[DependencyPackage]] = defaultdict(list)
self._use_latest: Collection[NormalizedName] = []
self._active_root_extras = (
frozenset(active_root_extras) if active_root_extras is not None else None
)

self._explicit_sources: dict[str, str] = {}
for package in locked or []:
Expand Down Expand Up @@ -457,7 +462,16 @@ def incompatibilities_for(
for dep in dependencies
if dep.name not in self.UNSAFE_PACKAGES
and self._python_constraint.allows_any(dep.python_constraint)
and (not self._env or dep.marker.validate(self._env.marker_env))
and (
not self._env
or dep.marker.validate(
self._marker_values(
self._active_root_extras
if dependency_package.package.is_root()
else dependency_package.dependency.extras
)
)
)
Comment on lines +465 to +474
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks like this change is not required by any test. To be honest, I have currently no idea if this change is redundant or if a test is missing...

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah yeah I wasn't sure what the best policy was here: I just updated the places where I saw markers being evaluated to include extras. What I found is that of the three changes in provider.py, any one of them is sufficient to pass the first round of tests. So I think there is some redundancy, but my thought is to not eliminate anything yet due to the issue with uncovered around not resolving conflicts with their own different extras.

]
dependencies = self._get_dependencies_with_overrides(_dependencies, package)

Expand Down Expand Up @@ -541,7 +555,12 @@ def complete_package(
if dep.name in self.UNSAFE_PACKAGES:
continue

if self._env and not dep.marker.validate(self._env.marker_env):
active_extras = (
self._active_root_extras if package.is_root() else dependency.extras
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems like the non root part is not tested yet. (Tests succeed when replacing dependency.extras with None.) Maybe, this path can be triggered by taking one of your tests and introducing an intermediate package?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great catch! While trying to add a test for this I uncovered an issue referenced in a top-level comment. The breaking test I've added doesn't include the intermediate package for simplicity but I can add it as we get to the bottom of that.

)
if self._env and not dep.marker.validate(
self._marker_values(active_extras)
):
continue

if not package.is_root() and (
Expand Down Expand Up @@ -601,7 +620,9 @@ def complete_package(

# For dependency resolution, markers of duplicate dependencies must be
# mutually exclusive.
active_extras = None if package.is_root() else dependency.extras
active_extras = (
self._active_root_extras if package.is_root() else dependency.extras
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems like that is not tested yet. (Tests succeed without this change.) You probably have to add a test similar to test_solver_resolves_duplicate_dependency_in_extra, which tests this for non root extras.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah yep, added test_solver_resolves_duplicate_dependency_in_root_extra(). But this results in both versions of A being selected, see the note about the bug here

)
deps = self._resolve_overlapping_markers(package, deps, active_extras)

if len(deps) == 1:
Expand Down Expand Up @@ -953,3 +974,17 @@ def _resolve_overlapping_markers(
# dependencies by constraint again. After overlapping markers were
# resolved, there might be new dependencies with the same constraint.
return self._merge_dependencies_by_constraint(new_dependencies)

def _marker_values(
self, extras: Collection[NormalizedName] | None = None
) -> dict[str, Any]:
"""
Marker values, from `self._env` if present plus the supplied extras

:param extras: the values to add to the 'extra' marker value
"""
result = self._env.marker_env.copy() if self._env is not None else {}
if extras is not None:
assert "extra" not in result, "'extra' marker key is already present in environment"
result["extra"] = set(extras)
return result
8 changes: 7 additions & 1 deletion src/poetry/puzzle/solver.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
installed: list[Package],
locked: list[Package],
io: IO,
active_root_extras: Collection[NormalizedName] | None = None,
) -> None:
self._package = package
self._pool = pool
Expand All @@ -49,7 +50,12 @@
self._io = io

self._provider = Provider(
self._package, self._pool, self._io, installed=installed, locked=locked
self._package,
self._pool,
self._io,
installed=installed,
locked=locked,
active_root_extras=active_root_extras,
)
self._overrides: list[dict[Package, dict[str, Dependency]]] = []

Expand Down Expand Up @@ -152,7 +158,7 @@
self._overrides.append(self._provider._overrides)

try:
result = resolve_version(self._package, self._provider)

Check failure on line 161 in src/poetry/puzzle/solver.py

View check run for this annotation

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

View check run for this annotation

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

View check run for this annotation

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

Check failure on line 161 in src/poetry/puzzle/solver.py

View check run for this annotation

Cirrus CI / Tests / FreeBSD (Python 3.9) / 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 0x30ad68bc7d30>

    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 0x30ad68bc71c0>
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 0x30ad68b901c0>
pool = <poetry.repositories.repository_pool.RepositoryPool object at 0x30ad68a3cc40>
locker = <tests.installation.test_installer.Locker object at 0x30ad68a3caf0>
installed = <tests.installation.test_installer.CustomInstalledRepository object at 0x30ad68e574f0>
repo = <poetry.repositories.repository.Repository object at 0x30ad68a3c070>
config = <tests.conftest.Config object at 0x30ad669f38b0>
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 0x30ad68bc7d30>

    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

View check run for this annotation

Cirrus CI / Tests / FreeBSD (Python 3.9) / 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 0x30ad68be9b20>

    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 0x30ad68be9880>
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 0x30ad68be9670>
pool = <poetry.repositories.repository_pool.RepositoryPool object at 0x30ad6932adc0>
locker = <tests.installation.test_installer.Locker object at 0x30ad6932aa00>
installed = <tests.installation.test_installer.CustomInstalledRepository object at 0x30ad6932aac0>
repo = <poetry.repositories.repository.Repository object at 0x30ad6932ac70>
config = <tests.conftest.Config object at 0x30ad65081bb0>
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 0x30ad68be9b20>

    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

View check run for this annotation

Cirrus CI / Tests / FreeBSD (Python 3.9) / 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 0x30ad6a0a1970>

    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 0x30ad6897db50>
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 0x30ad66afd430>
pool = <poetry.repositories.repository_pool.RepositoryPool object at 0x30ad6860f070>
locker = <tests.installation.test_installer.Locker object at 0x30ad6860f3d0>
installed = <tests.installation.test_installer.CustomInstalledRepository object at 0x30ad6860f100>
repo = <poetry.repositories.repository.Repository object at 0x30ad6860f790>
config = <tests.conftest.Config object at 0x30ad681ac700>
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 0x30ad6a0a1970>

    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

View check run for this annotation

Cirrus CI / Tests / FreeBSD (Python 3.11) / 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 0x3fe7a3e5f2d0>

    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 0x3fe7a5ef84d0>
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 0x3fe7a3ec0290>
pool = <poetry.repositories.repository_pool.RepositoryPool object at 0x3fe7a3fcd350>
locker = <tests.installation.test_installer.Locker object at 0x3fe7a3fcf8d0>
installed = <tests.installation.test_installer.CustomInstalledRepository object at 0x3fe7a3fcda10>
repo = <poetry.repositories.repository.Repository object at 0x3fe7a3fcc350>
config = <tests.conftest.Config object at 0x3fe7a25a3cd0>
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 0x3fe7a3e5f2d0>

    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

View check run for this annotation

Cirrus CI / Tests / FreeBSD (Python 3.11) / 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 0x2f70a96d8910>

    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 0x2f70a96d9810>
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 0x2f70aa3d9e90>
pool = <poetry.repositories.repository_pool.RepositoryPool object at 0x2f70ab3696d0>
locker = <tests.installation.test_installer.Locker object at 0x2f70ab3685d0>
installed = <tests.installation.test_installer.CustomInstalledRepository object at 0x2f70ab368f90>
repo = <poetry.repositories.repository.Repository object at 0x2f70ab369450>
config = <tests.conftest.Config object at 0x2f70ab2cadd0>
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 0x2f70a96d8910>

    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

View check run for this annotation

Cirrus CI / Tests / FreeBSD (Python 3.11) / 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 0x3fe7a58c4710>

    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 0x3fe7a58c6710>
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 0x3fe7a6c16a50>
pool = <poetry.repositories.repository_pool.RepositoryPool object at 0x3fe7a6af92d0>
locker = <tests.installation.test_installer.Locker object at 0x3fe7a6af9350>
installed = <tests.installation.test_installer.CustomInstalledRepository object at 0x3fe7a6af9d10>
repo = <poetry.repositories.repository.Repository object at 0x3fe7a6afbb10>
config = <tests.conftest.Config object at 0x3fe7a1a55c50>
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 0x3fe7a58c4710>

    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

View check run for this annotation

Cirrus CI / Tests / FreeBSD (Python 3.10) / 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 0x31c7a2c0f430>

    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 0x31c7a2ea1e70>
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 0x31c7a2c0dae0>
pool = <poetry.repositories.repository_pool.RepositoryPool object at 0x31c7a4420fa0>
locker = <tests.installation.test_installer.Locker object at 0x31c7a4422da0>
installed = <tests.installation.test_installer.CustomInstalledRepository object at 0x31c7a4422e00>
repo = <poetry.repositories.repository.Repository object at 0x31c7a44222f0>
config = <tests.conftest.Config object at 0x31c7a447b940>
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 0x31c7a2c0f430>

    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

View check run for this annotation

Cirrus CI / Tests / FreeBSD (Python 3.10) / 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 0x31c7a43aae00>

    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 0x31c7a43ab610>
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 0x31c7a28e0340>
pool = <poetry.repositories.repository_pool.RepositoryPool object at 0x31c7a43a4280>
locker = <tests.installation.test_installer.Locker object at 0x31c7a43a4910>
installed = <tests.installation.test_installer.CustomInstalledRepository object at 0x31c7a43a6470>
repo = <poetry.repositories.repository.Repository object at 0x31c7a43a44c0>
config = <tests.conftest.Config object at 0x31c7a4453820>
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 0x31c7a43aae00>

    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

View check run for this annotation

Cirrus CI / Tests / FreeBSD (Python 3.10) / 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 0x31c7a43de0b0>

    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 0x31c7a43de110>
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 0x31c7a43dfb80>
pool = <poetry.repositories.repository_pool.RepositoryPool object at 0x31c7a43dd030>
locker = <tests.installation.test_installer.Locker object at 0x31c7a43ddc90>
installed = <tests.installation.test_installer.CustomInstalledRepository object at 0x31c7a43dcfa0>
repo = <poetry.repositories.repository.Repository object at 0x31c7a43dd180>
config = <tests.conftest.Config object at 0x31c7a43ec370>
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 0x31c7a43de0b0>

    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

packages = result.packages
except OverrideNeeded as e:
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
[[package]]
name = "dependency"
version = "1.1.0"
description = ""
optional = true
python-versions = "*"
files = []

[[package]]
name = "dependency"
version = "1.2.0"
description = ""
optional = true
python-versions = "*"
files = []

[extras]
extra-one = ["dependency"]
extra-two = ["dependency"]

[metadata]
python-versions = "*"
lock-version = "2.0"
content-hash = "123456789"
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
[[package]]
name = "demo"
version = "1.0.0"
description = ""
optional = true
python-versions = "*"
files = []

[package.extras]
demo-extra-one = ["transitive-dep-one"]
demo-extra-two = ["transitive-dep-two"]

[package.dependencies]
transitive-dep-one = {version = "1.1.0", optional = true}
transitive-dep-two = {version = "1.2.0", optional = true}

[[package]]
name = "transitive-dep-one"
version = "1.1.0"
description = ""
optional = true
python-versions = "*"
files = []

[[package]]
name = "transitive-dep-two"
version = "1.2.0"
description = ""
optional = true
python-versions = "*"
files = []

[extras]
extra-one = ["demo", "demo"]
extra-two = ["demo", "demo"]

[metadata]
python-versions = "*"
lock-version = "2.0"
content-hash = "123456789"
34 changes: 34 additions & 0 deletions tests/installation/fixtures/with-exclusive-extras.test
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
[[package]]
name = "torch"
version = "1.11.0+cpu"
description = ""
optional = true
python-versions = "*"
files = []

[package.source]
reference = "pytorch-cpu"
type = "legacy"
url = "https://download.pytorch.org/whl/cpu"

[[package]]
name = "torch"
version = "1.11.0+cuda"
description = ""
optional = true
python-versions = "*"
files = []

[package.source]
reference = "pytorch-cuda"
type = "legacy"
url = "https://download.pytorch.org/whl/cuda"

[extras]
cpu = ["torch", "torch"]
cuda = ["torch", "torch"]

[metadata]
python-versions = "*"
lock-version = "2.0"
content-hash = "123456789"
Loading