diff --git a/src/poetry/puzzle/provider.py b/src/poetry/puzzle/provider.py index cfcbc6048e8..d55bd1d0499 100644 --- a/src/poetry/puzzle/provider.py +++ b/src/poetry/puzzle/provider.py @@ -125,7 +125,7 @@ def __init__( self._direct_origin = DirectOrigin(self._pool.artifact_cache) self._io = io self._env: Env | None = None - self._python_constraint = package.python_constraint + self._package_python_constraint = package.python_constraint self._is_debugging: bool = self._io.is_debug() or self._io.is_very_verbose() self._overrides: dict[Package, dict[str, Dependency]] = {} self._deferred_cache: dict[Dependency, Package] = {} @@ -159,11 +159,29 @@ def pool(self) -> RepositoryPool: def use_latest(self) -> Collection[NormalizedName]: return self._use_latest + @functools.cached_property + def _overrides_marker_intersection(self) -> BaseMarker: + overrides_marker_intersection: BaseMarker = AnyMarker() + for dep_overrides in self._overrides.values(): + for dep in dep_overrides.values(): + overrides_marker_intersection = overrides_marker_intersection.intersect( + dep.marker + ) + return overrides_marker_intersection + + @functools.cached_property + def _python_constraint(self) -> VersionConstraint: + return self._package_python_constraint.intersect( + get_python_constraint_from_marker(self._overrides_marker_intersection) + ) + def is_debugging(self) -> bool: return self._is_debugging def set_overrides(self, overrides: dict[Package, dict[str, Dependency]]) -> None: self._overrides = overrides + self.__dict__.pop("_python_constraint", None) + self.__dict__.pop("_overrides_marker_intersection", None) def load_deferred(self, load_deferred: bool) -> None: self._load_deferred = load_deferred @@ -180,16 +198,18 @@ def use_source_root(self, source_root: Path) -> Iterator[Provider]: @contextmanager def use_environment(self, env: Env) -> Iterator[Provider]: - original_python_constraint = self._python_constraint + original_python_constraint = self._package_python_constraint self._env = env - self._python_constraint = Version.parse(env.marker_env["python_full_version"]) + self._package_python_constraint = Version.parse( + env.marker_env["python_full_version"] + ) try: yield self finally: self._env = None - self._python_constraint = original_python_constraint + self._package_python_constraint = original_python_constraint @contextmanager def use_latest_for(self, names: Collection[NormalizedName]) -> Iterator[Provider]: @@ -627,16 +647,12 @@ def complete_package( continue # Sort out irrelevant requirements - overrides_marker_intersection: BaseMarker = AnyMarker() - for dep_overrides in self._overrides.values(): - for dep in dep_overrides.values(): - overrides_marker_intersection = ( - overrides_marker_intersection.intersect(dep.marker) - ) deps = [ dep for dep in deps - if not overrides_marker_intersection.intersect(dep.marker).is_empty() + if not self._overrides_marker_intersection.intersect( + dep.marker + ).is_empty() ] if len(deps) < 2: if not deps or (len(deps) == 1 and deps[0].constraint.is_empty()): diff --git a/tests/puzzle/test_solver.py b/tests/puzzle/test_solver.py index 944ef80d58c..6159f389630 100644 --- a/tests/puzzle/test_solver.py +++ b/tests/puzzle/test_solver.py @@ -58,7 +58,7 @@ def set_package_python_versions(provider: Provider, python_versions: str) -> None: provider._package.python_versions = python_versions - provider._python_constraint = provider._package.python_constraint + provider._package_python_constraint = provider._package.python_constraint def check_solver_result( @@ -4402,6 +4402,72 @@ def patched_choose_next(unsatisfied: list[Dependency]) -> Dependency: ) +@pytest.mark.parametrize("numpy_before_pandas", [False, True]) +@pytest.mark.parametrize("conflict", [False, True]) +def test_solver_should_not_raise_errors_for_irrelevant_transitive_python_constraints2( + solver: Solver, + repo: Repository, + package: ProjectPackage, + mocker: MockerFixture, + numpy_before_pandas: bool, + conflict: bool, +) -> None: + """This time with overrides.""" + package.python_versions = ">=3.6.2, <3.10" + set_package_python_versions(solver.provider, ">=3.6.2, <3.10") + package.add_dependency(Factory.create_dependency("pandas", ">=1")) + package.add_dependency( + Factory.create_dependency("numpy", {"version": ">=1.20.0", "python": ">=3.7"}) + ) + package.add_dependency( + Factory.create_dependency("numpy", {"version": "*", "python": "<3.7"}) + ) + + pandas = get_package("pandas", "1.1.5") + pandas.add_dependency(Factory.create_dependency("numpy", ">=1.15")) + + numpy_19 = get_package("numpy", "1.19") + numpy_19.python_versions = ">=3.6" + numpy_20 = get_package("numpy", "1.20") + numpy_20.python_versions = ">=3.8" if conflict else ">=3.7" + + repo.add_package(pandas) + repo.add_package(numpy_19) + repo.add_package(numpy_20) + + def patched_choose_next(unsatisfied: list[Dependency]) -> Dependency: + order = ( + ("root", "pandas", "numpy") + if numpy_before_pandas + else ("root", "numpy", "pandas") + ) + for preferred in order: + for dep in unsatisfied: + if dep.name == preferred: + return dep + raise RuntimeError + + mocker.patch( + "poetry.mixology.version_solver.VersionSolver._choose_next", + side_effect=patched_choose_next, + ) + + if conflict: + with pytest.raises(SolverProblemError): + solver.solve() + else: + transaction = solver.solve() + + check_solver_result( + transaction, + [ + {"job": "install", "package": numpy_19}, + {"job": "install", "package": numpy_20}, + {"job": "install", "package": pandas}, + ], + ) + + @pytest.mark.parametrize("is_locked", [False, True]) def test_solver_keeps_multiple_locked_dependencies_for_same_package( package: ProjectPackage,