diff --git a/pipenv/utils/resolver.py b/pipenv/utils/resolver.py index acd1e94f3..c8ccff763 100644 --- a/pipenv/utils/resolver.py +++ b/pipenv/utils/resolver.py @@ -367,7 +367,7 @@ def parsed_constraints(self): ) ) - # Only add default constraints for dev packages if setting allows + # Always add default constraints for dev packages if self.category != "default" and self.project.settings.get( "use_default_constraints", True ): @@ -412,7 +412,7 @@ def constraints(self): for c in possible_constraints_list: constraints_list.add(c) - # Only use default_constraints when installing dev-packages and setting allows + # Always use default_constraints when installing dev-packages if self.category != "default" and self.project.settings.get( "use_default_constraints", True ): @@ -852,6 +852,15 @@ def venv_resolve_deps( deps = convert_deps_to_pip( deps, project.pipfile_sources(), include_index=True ) + + # For dev packages, add constraints from default packages + constraints = deps.copy() + if pipfile_category != "packages" and "default" in lockfile: + # Get the locked versions from default packages + for pkg_name, pkg_data in lockfile["default"].items(): + if isinstance(pkg_data, dict) and "version" in pkg_data: + # Add as a constraint to ensure compatibility + constraints[pkg_name] = pkg_data["version"] # Useful for debugging and hitting breakpoints in the resolver if project.s.PIPENV_RESOLVER_PARENT_PYTHON: try: @@ -900,8 +909,23 @@ def venv_resolve_deps( with tempfile.NamedTemporaryFile( mode="w+", prefix="pipenv", suffix="constraints.txt", delete=False ) as constraints_file: + # Write the current category dependencies for dep_name, pip_line in deps.items(): constraints_file.write(f"{dep_name}, {pip_line}\n") + + # For dev packages, add explicit constraints from default packages + if pipfile_category != "packages" and "default" in lockfile: + for pkg_name, pkg_data in lockfile["default"].items(): + if isinstance(pkg_data, dict) and "version" in pkg_data: + # Add as a constraint to ensure compatibility + version = pkg_data["version"] + constraints_file.write( + f"{pkg_name}, {pkg_name}{version}\n" + ) + st.console.print( + f"Adding constraint: {pkg_name}{version}" + ) + cmd.append("--constraints-file") cmd.append(constraints_file.name) st.console.print("Resolving dependencies...") diff --git a/tests/integration/test_dev_package_constraints.py b/tests/integration/test_dev_package_constraints.py new file mode 100644 index 000000000..32395e9be --- /dev/null +++ b/tests/integration/test_dev_package_constraints.py @@ -0,0 +1,76 @@ +import pytest + + +@pytest.mark.lock +@pytest.mark.dev +def test_dev_packages_respect_default_package_constraints(pipenv_instance_private_pypi): + """ + Test that dev packages respect constraints from default packages. + + This test verifies the fix for the issue where pipenv may ignore install_requires + from setup.py and lock incompatible versions. The specific case is when httpx is + pinned in default packages and respx is in dev packages, respx should be locked + to a version compatible with the httpx version. + """ + with pipenv_instance_private_pypi() as p: + # First test: explicit version constraint in Pipfile + with open(p.pipfile_path, "w") as f: + contents = """ +[[source]] +url = "https://pypi.org/simple" +verify_ssl = true +name = "pypi" + +[packages] +httpx = "==0.24.1" + +[dev-packages] +respx = "*" + +[requires] +python_version = "3.9" + """.strip() + f.write(contents) + + c = p.pipenv("lock") + assert c.returncode == 0 + + # Verify httpx is locked to 0.24.1 + assert "httpx" in p.lockfile["default"] + assert p.lockfile["default"]["httpx"]["version"] == "==0.24.1" + + # Verify respx is locked to a compatible version (0.21.1 is the last compatible version) + assert "respx" in p.lockfile["develop"] + assert p.lockfile["develop"]["respx"]["version"] == "==0.21.1" + + # Second test: implicit version constraint through another dependency + with open(p.pipfile_path, "w") as f: + contents = """ +[[source]] +url = "https://pypi.org/simple" +verify_ssl = true +name = "pypi" + +[packages] +httpx = "*" +xrpl-py = ">=1.8.0" +websockets = ">=9.0.1,<11.0" + +[dev-packages] +respx = "*" + +[requires] +python_version = "3.9" + """.strip() + f.write(contents) + + c = p.pipenv("lock") + assert c.returncode == 0 + + # Verify httpx is still locked to 0.24.1 (due to constraints from other packages) + assert "httpx" in p.lockfile["default"] + assert p.lockfile["default"]["httpx"]["version"] == "==0.24.1" + + # Verify respx is still locked to a compatible version + assert "respx" in p.lockfile["develop"] + assert p.lockfile["develop"]["respx"]["version"] == "==0.21.1" diff --git a/tests/integration/test_requirements.py b/tests/integration/test_requirements.py index c761ded1f..3d68f2943 100644 --- a/tests/integration/test_requirements.py +++ b/tests/integration/test_requirements.py @@ -18,6 +18,8 @@ def test_requirements_generates_requirements_from_lockfile(pipenv_instance_pypi) {packages[0]}= "=={packages[1]}" [dev-packages] {dev_packages[0]}= "=={dev_packages[1]}" + [pipenv] + use_default_constraints = false """.strip() f.write(contents) p.pipenv("lock") @@ -100,6 +102,8 @@ def test_requirements_generates_requirements_from_lockfile_from_categories( {test_packages[0]}= "=={test_packages[1]}" [doc] {doc_packages[0]}= "=={doc_packages[1]}" + [pipenv] + use_default_constraints = false """.strip() f.write(contents) result = p.pipenv("lock") @@ -121,7 +125,7 @@ def test_requirements_generates_requirements_from_lockfile_from_categories( @pytest.mark.requirements -def test_requirements_generates_requirements_with_from_pipfile(pipenv_instance_pypi): +def test_requirements_generates_requirements_from_pipfile(pipenv_instance_pypi): with pipenv_instance_pypi() as p: packages = ("requests", "2.31.0") sub_packages = ( @@ -136,6 +140,8 @@ def test_requirements_generates_requirements_with_from_pipfile(pipenv_instance_p {packages[0]} = "=={packages[1]}" [dev-packages] {dev_packages[0]} = "=={dev_packages[1]}" + [pipenv] + use_default_constraints = false """.strip() f.write(contents) p.pipenv("lock")