Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix considered python constraint for overrides #10157

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 27 additions & 11 deletions src/poetry/puzzle/provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -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] = {}
Expand Down Expand Up @@ -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
Expand All @@ -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]:
Expand Down Expand Up @@ -629,16 +649,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()):
Expand Down
68 changes: 67 additions & 1 deletion tests/puzzle/test_solver.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -4445,6 +4445,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,
Expand Down