diff --git a/news/11457.removal.rst b/news/11457.removal.rst new file mode 100644 index 00000000000..e2a5bf3fc2a --- /dev/null +++ b/news/11457.removal.rst @@ -0,0 +1,2 @@ +Remove support for the legacy ``setup.py develop`` editable method. Pip now +requires setuptools >= 64. diff --git a/src/pip/_internal/commands/install.py b/src/pip/_internal/commands/install.py index 1ef7a0f4410..92d662cf083 100644 --- a/src/pip/_internal/commands/install.py +++ b/src/pip/_internal/commands/install.py @@ -59,7 +59,7 @@ running_under_virtualenv, virtualenv_no_global, ) -from pip._internal.wheel_builder import build, should_build_for_install_command +from pip._internal.wheel_builder import build logger = getLogger(__name__) @@ -425,9 +425,7 @@ def run(self, options: Values, args: list[str]) -> int: protect_pip_from_modification_on_windows(modifying_pip=modifying_pip) reqs_to_build = [ - r - for r in requirement_set.requirements_to_install - if should_build_for_install_command(r) + r for r in requirement_set.requirements_to_install if not r.is_wheel ] _, build_failures = build( diff --git a/src/pip/_internal/operations/install/editable_legacy.py b/src/pip/_internal/operations/install/editable_legacy.py deleted file mode 100644 index 0603d3d8819..00000000000 --- a/src/pip/_internal/operations/install/editable_legacy.py +++ /dev/null @@ -1,48 +0,0 @@ -"""Legacy editable installation process, i.e. `setup.py develop`.""" - -from __future__ import annotations - -import logging -from collections.abc import Sequence - -from pip._internal.build_env import BuildEnvironment -from pip._internal.utils.logging import indent_log -from pip._internal.utils.setuptools_build import make_setuptools_develop_args -from pip._internal.utils.subprocess import call_subprocess - -logger = logging.getLogger(__name__) - - -def install_editable( - *, - global_options: Sequence[str], - prefix: str | None, - home: str | None, - use_user_site: bool, - name: str, - setup_py_path: str, - isolated: bool, - build_env: BuildEnvironment, - unpacked_source_directory: str, -) -> None: - """Install a package in editable mode. Most arguments are pass-through - to setuptools. - """ - logger.info("Running setup.py develop for %s", name) - - args = make_setuptools_develop_args( - setup_py_path, - global_options=global_options, - no_user_config=isolated, - prefix=prefix, - home=home, - use_user_site=use_user_site, - ) - - with indent_log(): - with build_env: - call_subprocess( - args, - command_desc="python setup.py develop", - cwd=unpacked_source_directory, - ) diff --git a/src/pip/_internal/req/req_install.py b/src/pip/_internal/req/req_install.py index c9f6bff17e8..3ff535afe9d 100644 --- a/src/pip/_internal/req/req_install.py +++ b/src/pip/_internal/req/req_install.py @@ -37,9 +37,6 @@ from pip._internal.operations.build.metadata_legacy import ( generate_metadata as generate_metadata_legacy, ) -from pip._internal.operations.install.editable_legacy import ( - install_editable as install_editable_legacy, -) from pip._internal.operations.install.wheel import install_wheel from pip._internal.pyproject import load_pyproject_toml, make_pyproject_path from pip._internal.req.req_uninstall import UninstallPathSet @@ -827,43 +824,6 @@ def install( prefix=prefix, ) - if self.editable and not self.is_wheel: - deprecated( - reason=( - f"Legacy editable install of {self} (setup.py develop) " - "is deprecated." - ), - replacement=( - "to add a pyproject.toml or enable --use-pep517, " - "and use setuptools >= 64. " - "If the resulting installation is not behaving as expected, " - "try using --config-settings editable_mode=compat. " - "Please consult the setuptools documentation for more information" - ), - gone_in="25.3", - issue=11457, - ) - if self.config_settings: - logger.warning( - "--config-settings ignored for legacy editable install of %s. " - "Consider upgrading to a version of setuptools " - "that supports PEP 660 (>= 64).", - self, - ) - install_editable_legacy( - global_options=global_options if global_options is not None else [], - prefix=prefix, - home=home, - use_user_site=use_user_site, - name=self.req.name, - setup_py_path=self.setup_py_path, - isolated=self.isolated, - build_env=self.build_env, - unpacked_source_directory=self.unpacked_source_directory, - ) - self.install_succeeded = True - return - assert self.is_wheel assert self.local_file_path diff --git a/src/pip/_internal/utils/setuptools_build.py b/src/pip/_internal/utils/setuptools_build.py index 1b8c7a1c21f..57c6dde32e3 100644 --- a/src/pip/_internal/utils/setuptools_build.py +++ b/src/pip/_internal/utils/setuptools_build.py @@ -104,36 +104,6 @@ def make_setuptools_clean_args( return args -def make_setuptools_develop_args( - setup_py_path: str, - *, - global_options: Sequence[str], - no_user_config: bool, - prefix: str | None, - home: str | None, - use_user_site: bool, -) -> list[str]: - assert not (use_user_site and prefix) - - args = make_setuptools_shim_args( - setup_py_path, - global_options=global_options, - no_user_config=no_user_config, - ) - - args += ["develop", "--no-deps"] - - if prefix: - args += ["--prefix", prefix] - if home is not None: - args += ["--install-dir", home] - - if use_user_site: - args += ["--user", "--prefix="] - - return args - - def make_setuptools_egg_info_args( setup_py_path: str, egg_info_dir: str | None, diff --git a/src/pip/_internal/wheel_builder.py b/src/pip/_internal/wheel_builder.py index c5d260700af..ed992d709c7 100644 --- a/src/pip/_internal/wheel_builder.py +++ b/src/pip/_internal/wheel_builder.py @@ -43,37 +43,12 @@ def _contains_egg_info(s: str) -> bool: return bool(_egg_info_re.search(s)) -def _should_build( - req: InstallRequirement, -) -> bool: - """Return whether an InstallRequirement should be built into a wheel.""" - assert not req.constraint - - if req.is_wheel: - return False - - assert req.source_dir - - if req.editable: - # we only build PEP 660 editable requirements - return req.supports_pyproject_editable - - return True - - -def should_build_for_install_command( - req: InstallRequirement, -) -> bool: - return _should_build(req) - - def _should_cache( req: InstallRequirement, ) -> bool | None: """ Return whether a built InstallRequirement can be stored in the persistent - wheel cache, assuming the wheel cache is available, and _should_build() - has determined a wheel needs to be built. + wheel cache, assuming the wheel cache is available. """ if req.editable or not req.source_dir: # never cache editable requirements diff --git a/tests/data/packages/SetupPyUTF8/setup.py b/tests/data/packages/SetupPyUTF8/setup.py index bd9fd2a294b..026d3a215e6 100644 --- a/tests/data/packages/SetupPyUTF8/setup.py +++ b/tests/data/packages/SetupPyUTF8/setup.py @@ -1,4 +1,4 @@ -from distutils.core import setup +from setuptools import setup setup( name="SetupPyUTF8", diff --git a/tests/functional/test_freeze.py b/tests/functional/test_freeze.py index 9883beb87fd..4079f9a9485 100644 --- a/tests/functional/test_freeze.py +++ b/tests/functional/test_freeze.py @@ -24,7 +24,6 @@ need_svn, wheel, ) -from tests.lib.direct_url import get_created_direct_url_path from tests.lib.venv import VirtualEnvironment distribute_re = re.compile("^distribute==[0-9.]+\n", re.MULTILINE) @@ -1039,7 +1038,7 @@ def test_freeze_pep610_editable(script: PipTestEnvironment) -> None: """ pkg_path = _create_test_package(script.scratch_path, name="testpkg") result = script.pip("install", pkg_path) - direct_url_path = get_created_direct_url_path(result, "testpkg") + direct_url_path = result.get_created_direct_url_path("testpkg") assert direct_url_path # patch direct_url.json to simulate an editable install with open(direct_url_path) as f: diff --git a/tests/functional/test_install.py b/tests/functional/test_install.py index a1bd81d31d0..7d2bb35ec5c 100644 --- a/tests/functional/test_install.py +++ b/tests/functional/test_install.py @@ -16,6 +16,7 @@ import pytest from pip._internal.cli.status_codes import ERROR, SUCCESS +from pip._internal.models.direct_url import DirectUrl from pip._internal.models.index import PyPI, TestPyPI from pip._internal.utils.misc import rmtree from pip._internal.utils.urls import path_to_url @@ -437,7 +438,9 @@ def test_install_editable_uninstalls_existing( to_install = data.packages.joinpath("pip_test_package-0.1.tar.gz") result = script.pip_install_local(to_install) assert "Successfully installed pip-test-package" in result.stdout - result.assert_installed("piptestpackage", editable=False) + result.assert_installed( + "piptestpackage", dist_name="pip-test-package", editable=False + ) result = script.pip( "install", @@ -449,7 +452,9 @@ def test_install_editable_uninstalls_existing( ) ), ) - result.assert_installed("pip-test-package", with_files=[".git"]) + result.assert_installed( + "piptestpackage", dist_name="pip-test-package", with_files=[".git"] + ) assert "Found existing installation: pip-test-package 0.1" in result.stdout assert "Uninstalling pip-test-package-" in result.stdout assert "Successfully uninstalled pip-test-package" in result.stdout @@ -469,13 +474,8 @@ def test_install_editable_uninstalls_existing_from_path( result.assert_installed("simplewheel", editable=False) result.did_create(simple_folder) - result = script.pip( - "install", - "-e", - to_install, - ) - install_path = script.site_packages / "simplewheel.egg-link" - result.did_create(install_path) + result = script.pip_install_local("-e", to_install, "-v") + script.assert_installed_editable("simplewheel") assert "Found existing installation: simplewheel 1.0" in result.stdout assert "Uninstalling simplewheel-" in result.stdout assert "Successfully uninstalled simplewheel" in result.stdout @@ -571,7 +571,6 @@ def test_basic_install_relative_directory( Test installing a requirement using a relative path. """ dist_info_folder = script.site_packages / "fspkg-0.1.dev0.dist-info" - egg_link_file = script.site_packages / "FSPkg.egg-link" package_folder = script.site_packages / "fspkg" # Compute relative install path to FSPkg from scratch path. @@ -595,7 +594,9 @@ def test_basic_install_relative_directory( else: # Editable install. result = script.pip("install", "-e", req_path, cwd=script.scratch_path) - result.did_create(egg_link_file) + direct_url = result.get_created_direct_url("fspkg") + assert direct_url + assert direct_url.is_local_editable() def test_install_quiet(script: PipTestEnvironment, data: TestData) -> None: @@ -670,8 +671,7 @@ def test_link_hash_pass_require_hashes( considered valid for --require-hashes.""" url = path_to_url(str(shared_data.packages.joinpath("simple-1.0.tar.gz"))) url = ( - f"{url}#sha256=" - "393043e672415891885c9a2a0929b1af95fb866d6ca016b42d2e6ce53619b653" + f"{url}#sha256=393043e672415891885c9a2a0929b1af95fb866d6ca016b42d2e6ce53619b653" ) script.pip_install_local("--no-deps", "--require-hashes", url) @@ -895,41 +895,6 @@ def test_editable_install__local_dir_no_setup_py( ) -@pytest.mark.skipif( - sys.version_info >= (3, 12), - reason="Setuptools<64 does not support Python 3.12+", -) -@pytest.mark.network -def test_editable_install_legacy__local_dir_no_setup_py_with_pyproject( - script: PipTestEnvironment, -) -> None: - """ - Test installing in legacy editable mode from a local directory with no - setup.py but that does have pyproject.toml with a build backend that does - not support the build_editable hook. - """ - local_dir = script.scratch_path.joinpath("temp") - local_dir.mkdir() - pyproject_path = local_dir.joinpath("pyproject.toml") - pyproject_path.write_text( - textwrap.dedent( - """ - [build-system] - requires = ["setuptools<64"] - build-backend = "setuptools.build_meta" - """ - ) - ) - - result = script.pip("install", "-e", local_dir, expect_error=True) - assert not result.files_created - - msg = result.stderr - assert "has a 'pyproject.toml'" in msg - assert "does not have a 'setup.py' nor a 'setup.cfg'" in msg - assert "cannot be installed in editable mode" in msg - - def test_editable_install__local_dir_setup_requires_with_pyproject( script: PipTestEnvironment, shared_data: TestData ) -> None: @@ -1405,7 +1370,9 @@ def _test_install_editable_with_prefix( result = script.pip("install", "--editable", pkga_path, "--prefix", prefix_path) # assert pkga is installed at correct location - install_path = script.scratch / site_packages / "pkga.egg-link" + install_path = ( + script.scratch / site_packages / "pkga-0.1.dist-info" / "direct_url.json" + ) result.did_create(install_path) return result @@ -1417,13 +1384,13 @@ def test_install_editable_with_target(script: PipTestEnvironment) -> None: pkg_path.mkdir() pkg_path.joinpath("setup.py").write_text( textwrap.dedent( + """\ + from setuptools import setup + setup( + name='pkg', + install_requires=['watching_testrunner'] + ) """ - from setuptools import setup - setup( - name='pkg', - install_requires=['watching_testrunner'] - ) - """ ) ) @@ -1431,7 +1398,11 @@ def test_install_editable_with_target(script: PipTestEnvironment) -> None: target.mkdir() result = script.pip("install", "--editable", pkg_path, "--target", target) - result.did_create(script.scratch / "target" / "pkg.egg-link") + direct_url_path = result.get_created_direct_url_path("pkg") + assert direct_url_path + assert direct_url_path.parent.parent == target + direct_url = DirectUrl.from_json(direct_url_path.read_text()) + assert direct_url.is_local_editable() result.did_create(script.scratch / "target" / "watching_testrunner.py") @@ -1443,28 +1414,6 @@ def test_install_editable_with_prefix_setup_py(script: PipTestEnvironment) -> No _test_install_editable_with_prefix(script, {"setup.py": setup_py}) -@pytest.mark.skipif( - sys.version_info >= (3, 12), - reason="Setuptools<64 does not support Python 3.12+", -) -@pytest.mark.network -def test_install_editable_legacy_with_prefix_setup_cfg( - script: PipTestEnvironment, -) -> None: - setup_cfg = """[metadata] -name = pkga -version = 0.1 -""" - pyproject_toml = """[build-system] -requires = ["setuptools<64", "wheel"] -build-backend = "setuptools.build_meta" -""" - result = _test_install_editable_with_prefix( - script, {"setup.cfg": setup_cfg, "pyproject.toml": pyproject_toml} - ) - assert "(setup.py develop) is deprecated" in result.stderr - - def test_install_package_conflict_prefix_and_user( script: PipTestEnvironment, data: TestData ) -> None: diff --git a/tests/functional/test_install_direct_url.py b/tests/functional/test_install_direct_url.py index 5aefab09cb3..ef6d953c178 100644 --- a/tests/functional/test_install_direct_url.py +++ b/tests/functional/test_install_direct_url.py @@ -3,21 +3,11 @@ from pip._internal.models.direct_url import VcsInfo from tests.lib import PipTestEnvironment, TestData, _create_test_package -from tests.lib.direct_url import get_created_direct_url def test_install_find_links_no_direct_url(script: PipTestEnvironment) -> None: result = script.pip_install_local("simple") - assert not get_created_direct_url(result, "simple") - - -def test_install_vcs_editable_no_direct_url(script: PipTestEnvironment) -> None: - pkg_path = _create_test_package(script.scratch_path, name="testpkg") - args = ["install", "-e", f"git+{pkg_path.as_uri()}#egg=testpkg"] - result = script.pip(*args) - # legacy editable installs do not generate .dist-info, - # hence no direct_url.json - assert not get_created_direct_url(result, "testpkg") + assert not result.get_created_direct_url("simple") def test_install_vcs_non_editable_direct_url(script: PipTestEnvironment) -> None: @@ -25,7 +15,7 @@ def test_install_vcs_non_editable_direct_url(script: PipTestEnvironment) -> None url = pkg_path.as_uri() args = ["install", f"git+{url}#egg=testpkg"] result = script.pip(*args) - direct_url = get_created_direct_url(result, "testpkg") + direct_url = result.get_created_direct_url("testpkg") assert direct_url assert direct_url.url == url assert isinstance(direct_url.info, VcsInfo) @@ -36,7 +26,7 @@ def test_install_archive_direct_url(script: PipTestEnvironment, data: TestData) req = "simple @ " + data.packages.joinpath("simple-2.0.tar.gz").as_uri() assert req.startswith("simple @ file://") result = script.pip("install", req) - assert get_created_direct_url(result, "simple") + assert result.get_created_direct_url("simple") @pytest.mark.network @@ -48,7 +38,7 @@ def test_install_vcs_constraint_direct_url(script: PipTestEnvironment) -> None: "#egg=pip-test-package" ) result = script.pip("install", "pip-test-package", "-c", constraints_file) - assert get_created_direct_url(result, "pip_test_package") + assert result.get_created_direct_url("pip_test_package") def test_install_vcs_constraint_direct_file_url(script: PipTestEnvironment) -> None: @@ -57,4 +47,4 @@ def test_install_vcs_constraint_direct_file_url(script: PipTestEnvironment) -> N constraints_file = script.scratch_path / "constraints.txt" constraints_file.write_text(f"git+{url}#egg=testpkg") result = script.pip("install", "testpkg", "-c", constraints_file) - assert get_created_direct_url(result, "testpkg") + assert result.get_created_direct_url("testpkg") diff --git a/tests/functional/test_install_reqs.py b/tests/functional/test_install_reqs.py index 3c5b6db4a68..dbcf5ebe0d1 100644 --- a/tests/functional/test_install_reqs.py +++ b/tests/functional/test_install_reqs.py @@ -217,7 +217,6 @@ def test_relative_requirements_file( """ dist_info_folder = script.site_packages / "fspkg-0.1.dev0.dist-info" - egg_link_file = script.site_packages / "FSPkg.egg-link" package_folder = script.site_packages / "fspkg" # Compute relative install path to FSPkg from scratch path. @@ -249,7 +248,9 @@ def test_relative_requirements_file( result = script.pip( "install", "-vvv", "-r", reqs_file.name, cwd=script.scratch_path ) - result.did_create(egg_link_file) + direct_url = result.get_created_direct_url("fspkg") + assert direct_url + assert direct_url.is_local_editable() @pytest.mark.xfail @@ -384,9 +385,8 @@ def test_install_local_editable_with_extras( res = script.pip_install_local( "-e", f"{to_install}[bar]", allow_stderr_warning=True ) - res.did_update(script.site_packages / "easy-install.pth") - res.did_create(script.site_packages / "LocalExtras.egg-link") - res.did_create(script.site_packages / "simple") + res.assert_installed("LocalExtras", editable=True, editable_vcs=False) + res.assert_installed("simple", editable=False) def test_install_collected_dependencies_first(script: PipTestEnvironment) -> None: diff --git a/tests/functional/test_install_user.py b/tests/functional/test_install_user.py index 0b1e025cbbe..ee8893b50ce 100644 --- a/tests/functional/test_install_user.py +++ b/tests/functional/test_install_user.py @@ -75,7 +75,7 @@ def test_install_subversion_usersite_editable_with_distribute( ) ), ) - result.assert_installed("INITools", use_user_site=True) + result.assert_installed("INITools") def test_install_from_current_directory_into_usersite( self, script: PipTestEnvironment, data: TestData diff --git a/tests/functional/test_install_vcs_git.py b/tests/functional/test_install_vcs_git.py index 38d416fb62d..38aa569683c 100644 --- a/tests/functional/test_install_vcs_git.py +++ b/tests/functional/test_install_vcs_git.py @@ -183,7 +183,9 @@ def test_install_editable_from_git_with_https( url_path = "pypa/pip-test-package.git" local_url = _github_checkout(url_path, tmpdir, egg="pip-test-package") result = script.pip("install", "-e", local_url) - result.assert_installed("pip-test-package", with_files=[".git"]) + result.assert_installed( + "piptestpackage", dist_name="pip-test-package", with_files=[".git"] + ) @pytest.mark.network @@ -193,11 +195,12 @@ def test_install_noneditable_git(script: PipTestEnvironment) -> None: """ result = script.pip( "install", - "git+https://github.com/pypa/pip-test-package.git" - "@0.1.1#egg=pip-test-package", + "git+https://github.com/pypa/pip-test-package.git@0.1.1#egg=pip-test-package", ) dist_info_folder = script.site_packages / "pip_test_package-0.1.1.dist-info" - result.assert_installed("piptestpackage", without_egg_link=True, editable=False) + result.assert_installed( + "piptestpackage", dist_name="pip-test-package", editable=False + ) result.did_create(dist_info_folder) @@ -339,29 +342,6 @@ def test_install_git_logs_commit_sha( assert f"Resolved {base_local_url[4:]} to commit {expected_sha}" in result.stdout -@pytest.mark.network -def test_git_with_tag_name_and_update(script: PipTestEnvironment, tmpdir: Path) -> None: - """ - Test cloning a git repository and updating to a different version. - """ - url_path = "pypa/pip-test-package.git" - base_local_url = _github_checkout(url_path, tmpdir) - - local_url = f"{base_local_url}#egg=pip-test-package" - result = script.pip("install", "-e", local_url) - result.assert_installed("pip-test-package", with_files=[".git"]) - - new_local_url = f"{base_local_url}@0.1.2#egg=pip-test-package" - result = script.pip( - "install", - "--global-option=--version", - "-e", - new_local_url, - allow_stderr_warning=True, - ) - assert "0.1.2" in result.stdout - - @pytest.mark.network def test_git_branch_should_not_be_changed( script: PipTestEnvironment, tmpdir: Path diff --git a/tests/functional/test_install_wheel.py b/tests/functional/test_install_wheel.py index e01ecfb57f3..3de7560372e 100644 --- a/tests/functional/test_install_wheel.py +++ b/tests/functional/test_install_wheel.py @@ -38,7 +38,7 @@ def test_install_from_future_wheel_version( result = script.pip("install", package, "--no-index", expect_error=True) with pytest.raises(TestFailure): - result.assert_installed("futurewheel", without_egg_link=True, editable=False) + result.assert_installed("futurewheel", editable=False) package = make_wheel_with_file( name="futurewheel", @@ -46,7 +46,7 @@ def test_install_from_future_wheel_version( wheel_metadata_updates={"Wheel-Version": "1.9"}, ).save_to_dir(tmpdir) result = script.pip("install", package, "--no-index", expect_stderr=True) - result.assert_installed("futurewheel", without_egg_link=True, editable=False) + result.assert_installed("futurewheel", editable=False) @pytest.mark.parametrize( @@ -67,7 +67,7 @@ def test_install_from_broken_wheel( package = data.packages.joinpath(wheel_name) result = script.pip("install", package, "--no-index", expect_error=True) with pytest.raises(TestFailure): - result.assert_installed("futurewheel", without_egg_link=True, editable=False) + result.assert_installed("futurewheel", editable=False) def test_basic_install_from_wheel( diff --git a/tests/functional/test_list.py b/tests/functional/test_list.py index 468e6acd6c2..774cb51869a 100644 --- a/tests/functional/test_list.py +++ b/tests/functional/test_list.py @@ -15,7 +15,6 @@ make_wheel, wheel, ) -from tests.lib.direct_url import get_created_direct_url_path @pytest.fixture(scope="session") @@ -28,6 +27,7 @@ def simple_script( script = script_factory(tmpdir.joinpath("workspace")) script.pip( "install", + "--no-build-isolation", "-f", shared_data.find_links, "--no-index", @@ -338,7 +338,14 @@ def pip_test_package_script( ) -> PipTestEnvironment: tmpdir = tmpdir_factory.mktemp("pip_test_package") script = script_factory(tmpdir.joinpath("workspace")) - script.pip("install", "-f", shared_data.find_links, "--no-index", "simple==1.0") + script.pip( + "install", + "--no-build-isolation", + "-f", + shared_data.find_links, + "--no-index", + "simple==1.0", + ) script.pip( "install", "-e", @@ -736,7 +743,7 @@ def test_list_pep610_editable(script: PipTestEnvironment) -> None: """ pkg_path = _create_test_package(script.scratch_path, name="testpkg") result = script.pip("install", pkg_path) - direct_url_path = get_created_direct_url_path(result, "testpkg") + direct_url_path = result.get_created_direct_url_path("testpkg") assert direct_url_path # patch direct_url.json to simulate an editable install with open(direct_url_path) as f: diff --git a/tests/functional/test_new_resolver.py b/tests/functional/test_new_resolver.py index c094e675a30..a5b61454c86 100644 --- a/tests/functional/test_new_resolver.py +++ b/tests/functional/test_new_resolver.py @@ -5,7 +5,6 @@ from typing import TYPE_CHECKING, Callable, Protocol import pytest -from packaging.utils import canonicalize_name from tests.conftest import ScriptFactory from tests.lib import ( @@ -14,27 +13,12 @@ create_basic_wheel_for_package, create_test_package_with_setup, ) -from tests.lib.direct_url import get_created_direct_url from tests.lib.venv import VirtualEnvironment from tests.lib.wheel import make_wheel MakeFakeWheel = Callable[[str, str, str], pathlib.Path] -def assert_editable(script: PipTestEnvironment, *args: str) -> None: - # This simply checks whether all of the listed packages have a - # corresponding .egg-link file installed. - # TODO: Implement a more rigorous way to test for editable installations. - egg_links = {f"{canonicalize_name(arg)}.egg-link" for arg in args} - actual_egg_links = { - f"{canonicalize_name(p.stem)}.egg-link" - for p in script.site_packages_path.glob("*.egg-link") - } - assert ( - egg_links <= actual_egg_links - ), f"{args!r} not all found in {script.site_packages_path!r}" - - @pytest.fixture def make_fake_wheel(script: PipTestEnvironment) -> MakeFakeWheel: def _make_fake_wheel(name: str, version: str, wheel_tag: str) -> pathlib.Path: @@ -340,7 +324,7 @@ def test_new_resolver_installs_editable(script: PipTestEnvironment) -> None: source_dir, ) script.assert_installed(base="0.1.0", dep="0.1.0") - assert_editable(script, "dep") + script.assert_installed_editable("dep") @pytest.mark.parametrize( @@ -1872,7 +1856,7 @@ def test_new_resolver_succeeds_on_matching_constraint_and_requirement( script.assert_installed(test_pkg="0.1.0") if editable: - assert_editable(script, "test_pkg") + script.assert_installed_editable("test_pkg") def test_new_resolver_applies_url_constraint_to_dep(script: PipTestEnvironment) -> None: @@ -2155,9 +2139,9 @@ def test_new_resolver_direct_url_with_extras( ) script.assert_installed(pkg1="1", pkg2="1", pkg3="1") - assert not get_created_direct_url(result, "pkg1") - assert get_created_direct_url(result, "pkg2") - assert not get_created_direct_url(result, "pkg3") + assert not result.get_created_direct_url("pkg1") + assert result.get_created_direct_url("pkg2") + assert not result.get_created_direct_url("pkg3") def test_new_resolver_modifies_installed_incompatible( diff --git a/tests/functional/test_pep660.py b/tests/functional/test_pep660.py index 9cf3da31172..1c27530e301 100644 --- a/tests/functional/test_pep660.py +++ b/tests/functional/test_pep660.py @@ -164,13 +164,11 @@ def test_install_pep660_from_reqs_file( ), "a .egg-link file should not have been created" -def test_install_no_pep660_setup_py_fallback( - tmpdir: Path, script: PipTestEnvironment -) -> None: +def test_install_no_pep660(tmpdir: Path, script: PipTestEnvironment) -> None: """ - Test that we fall back to setuptools develop when using a backend that - does not support build_editable. Since there is a pyproject.toml, - the prepare_metadata_for_build_wheel hook is called. + Test the error message when the build backend does not support PEP 660. + Since there is a pyproject.toml, the prepare_metadata_for_build_wheel hook + is called. """ project_dir = _make_project(tmpdir, BACKEND_WITHOUT_PEP660, with_setup_py=True) result = script.pip( @@ -179,38 +177,14 @@ def test_install_no_pep660_setup_py_fallback( "--no-build-isolation", "--editable", project_dir, - allow_stderr_warning=False, + allow_error=True, ) + assert result.returncode != 0 _assert_hook_called(project_dir, "prepare_metadata_for_build_wheel") assert ( - result.test_env.site_packages.joinpath("project.egg-link") - in result.files_created - ), "a .egg-link file should have been created" - - -def test_install_no_pep660_setup_cfg_fallback( - tmpdir: Path, script: PipTestEnvironment -) -> None: - """ - Test that we fall back to setuptools develop when using a backend that - does not support build_editable. Since there is a pyproject.toml, - the prepare_metadata_for_build_wheel hook is called. - """ - project_dir = _make_project(tmpdir, BACKEND_WITHOUT_PEP660, with_setup_py=False) - result = script.pip( - "install", - "--no-index", - "--no-build-isolation", - "--editable", - project_dir, - allow_stderr_warning=False, + "Cannot build editable project because " + "the build backend does not have the build_editable hook" in result.stderr ) - print(result.stdout, result.stderr) - _assert_hook_called(project_dir, "prepare_metadata_for_build_wheel") - assert ( - result.test_env.site_packages.joinpath("project.egg-link") - in result.files_created - ), ".egg-link file should have been created" def test_wheel_editable_pep660_basic(tmpdir: Path, script: PipTestEnvironment) -> None: diff --git a/tests/functional/test_show.py b/tests/functional/test_show.py index ea831935372..2d2ee483561 100644 --- a/tests/functional/test_show.py +++ b/tests/functional/test_show.py @@ -36,7 +36,7 @@ def test_show_with_files_not_found(script: PipTestEnvironment, data: TestData) - installed-files.txt not found. """ editable = data.packages.joinpath("SetupPyUTF8") - script.pip("install", "-e", editable) + script.run("python", "setup.py", "develop", cwd=editable) result = script.pip("show", "-f", "SetupPyUTF8") lines = result.stdout.splitlines() assert len(lines) == 13 diff --git a/tests/functional/test_uninstall.py b/tests/functional/test_uninstall.py index 7d041bb69e0..c00b4e975ae 100644 --- a/tests/functional/test_uninstall.py +++ b/tests/functional/test_uninstall.py @@ -437,7 +437,7 @@ def _test_uninstall_editable_with_source_outside_venv( temp_pkg_dir, expect_stderr=True, ) - result2 = script.pip("install", "-e", temp_pkg_dir) + result2 = script.run("python", "setup.py", "develop", cwd=temp_pkg_dir) result2.did_create(join(script.site_packages, "pip-test-package.egg-link")) result3 = script.pip("uninstall", "-y", "pip-test-package") assert_all_changes( @@ -665,7 +665,7 @@ def test_uninstall_editable_and_pip_install( script.environ["SETUPTOOLS_SYS_PATH_TECHNIQUE"] = "raw" pkg_path = data.packages.joinpath("FSPkg") - script.pip("install", "-e", ".", expect_stderr=True, cwd=pkg_path) + script.run("python", "setup.py", "develop", expect_stderr=True, cwd=pkg_path) # ensure both are installed with --ignore-installed: script.pip("install", "--ignore-installed", ".", expect_stderr=True, cwd=pkg_path) script.assert_installed(FSPkg="0.1.dev0") @@ -702,7 +702,7 @@ def test_uninstall_editable_and_pip_install_easy_install_remove( # Install FSPkg pkg_path = data.packages.joinpath("FSPkg") - script.pip("install", "-e", ".", expect_stderr=True, cwd=pkg_path) + script.run("python", "setup.py", "develop", expect_stderr=True, cwd=pkg_path) # Rename easy-install.pth to pip-test-fspkg.pth easy_install_pth = join(script.site_packages_path, "easy-install.pth") diff --git a/tests/functional/test_uninstall_user.py b/tests/functional/test_uninstall_user.py index 63ba5e581d0..25b7512e12c 100644 --- a/tests/functional/test_uninstall_user.py +++ b/tests/functional/test_uninstall_user.py @@ -92,7 +92,9 @@ def test_uninstall_editable_from_usersite( # install to_install = data.packages.joinpath("FSPkg") - result1 = script.pip("install", "--user", "-e", to_install) + result1 = script.run( + "python", "setup.py", "develop", "--user", "--prefix=", cwd=to_install + ) egg_link = script.user_site / "FSPkg.egg-link" result1.did_create(egg_link) diff --git a/tests/lib/__init__.py b/tests/lib/__init__.py index 78fe3604480..4811ff8157f 100644 --- a/tests/lib/__init__.py +++ b/tests/lib/__init__.py @@ -14,6 +14,7 @@ from contextlib import contextmanager from hashlib import sha256 from io import BytesIO, StringIO +from pathlib import Path from textwrap import dedent from typing import Any, AnyStr, Callable, Literal, Protocol, Union, cast from urllib.request import pathname2url @@ -28,11 +29,11 @@ from pip._internal.index.collector import LinkCollector from pip._internal.index.package_finder import PackageFinder from pip._internal.locations import get_major_minor_version +from pip._internal.models.direct_url import DIRECT_URL_METADATA_NAME, DirectUrl from pip._internal.models.search_scope import SearchScope from pip._internal.models.selection_prefs import SelectionPreferences from pip._internal.models.target_python import TargetPython from pip._internal.network.session import PipSession -from pip._internal.utils.egg_link import _egg_link_names from tests.lib.venv import VirtualEnvironment from tests.lib.wheel import make_wheel @@ -292,90 +293,71 @@ def files_updated(self) -> FoundFiles: def files_deleted(self) -> FoundFiles: return FoundFiles(self._impl.files_deleted) - def _get_egg_link_path_created(self, egg_link_paths: list[str]) -> str | None: - for egg_link_path in egg_link_paths: - if egg_link_path in self.files_created: - return egg_link_path + def get_created_direct_url_path(self, pkg: str) -> Path | None: + dist_info_prefix = canonicalize_name(pkg).replace("-", "_") + "-" + for filename in self.files_created: + if ( + filename.name == DIRECT_URL_METADATA_NAME + and filename.parent.name.endswith(".dist-info") + and filename.parent.name.startswith(dist_info_prefix) + ): + return self.test_env.base_path / filename + return None + + def get_created_direct_url(self, pkg: str) -> DirectUrl | None: + direct_url_path = self.get_created_direct_url_path(pkg) + if direct_url_path: + with open(direct_url_path) as f: + return DirectUrl.from_json(f.read()) return None def assert_installed( self, pkg_name: str, + *, + dist_name: str | None = None, editable: bool = True, + editable_vcs: bool = True, with_files: list[str] | None = None, without_files: list[str] | None = None, - without_egg_link: bool = False, - use_user_site: bool = False, sub_dir: str | None = None, ) -> None: + if dist_name is None: + dist_name = pkg_name with_files = with_files or [] without_files = without_files or [] e = self.test_env - if editable: - pkg_dir = e.venv / "src" / canonicalize_name(pkg_name) + if editable and editable_vcs: + pkg_dir = e.venv / "src" / canonicalize_name(dist_name) # If package was installed in a sub directory if sub_dir: pkg_dir = pkg_dir / sub_dir + elif editable and not editable_vcs: + pkg_dir = None + assert not with_files + assert not without_files else: - without_egg_link = True pkg_dir = e.site_packages / pkg_name - if use_user_site: - egg_link_paths = [ - e.user_site / egg_link_name - for egg_link_name in _egg_link_names(pkg_name) - ] - else: - egg_link_paths = [ - e.site_packages / egg_link_name - for egg_link_name in _egg_link_names(pkg_name) - ] - - egg_link_path_created = self._get_egg_link_path_created(egg_link_paths) - if without_egg_link: - if egg_link_path_created: + direct_url = self.get_created_direct_url(dist_name) + if not editable: + if direct_url and direct_url.is_local_editable(): raise TestFailure( - f"unexpected egg link file created: {egg_link_path_created!r}\n" + "unexpected editable direct_url.json created: " + f"{self.get_created_direct_url_path(dist_name)!r}\n" f"{self}" ) else: - if not egg_link_path_created: - raise TestFailure( - f"expected egg link file missing: {egg_link_paths!r}\n{self}" - ) - - egg_link_file = self.files_created[egg_link_path_created] - egg_link_contents = egg_link_file.bytes.replace(os.linesep, "\n") - - # FIXME: I don't understand why there's a trailing . here - if not ( - egg_link_contents.endswith("\n.") - and egg_link_contents[:-2].endswith(os.fspath(pkg_dir)) - ): - expected_ending = f"{pkg_dir}\n." + if not direct_url or not direct_url.is_local_editable(): raise TestFailure( - textwrap.dedent( - f""" - Incorrect egg_link file {egg_link_file!r} - Expected ending: {expected_ending!r} - ------- Actual contents ------- - {egg_link_contents!r} - ------------------------------- - """ - ).strip() + f"{dist_name!r} not installed as editable: direct_url.json " + "not found or not editable\n" + f"{self.get_created_direct_url_path(dist_name)!r}\n" + f"{self}" ) - if use_user_site: - pth_file = e.user_site / "easy-install.pth" - else: - pth_file = e.site_packages / "easy-install.pth" - - if (pth_file in self.files_updated) == without_egg_link: - maybe = "" if without_egg_link else "not " - raise TestFailure(f"{pth_file} unexpectedly {maybe}updated by install") - - if (pkg_dir in self.files_created) == (os.curdir in without_files): + if pkg_dir and (pkg_dir in self.files_created) == (os.curdir in without_files): maybe = "not " if os.curdir in without_files else "" files = sorted(p.as_posix() for p in self.files_created) raise TestFailure( @@ -756,6 +738,17 @@ def assert_not_installed(self, *args: str) -> None: expected = {canonicalize_name(k) for k in args} assert not (expected & installed), f"{expected!r} contained in {installed!r}" + def assert_installed_editable(self, dist_name: str) -> None: + dist_name = canonicalize_name(dist_name) + ret = self.pip("list", "--format=json") + installed = json.loads(ret.stdout) + assert any( + x + for x in installed + if canonicalize_name(x["name"]) == dist_name + and x.get("editable_project_location") + ) + # FIXME ScriptTest does something similar, but only within a single # ProcResult; this generalizes it so states can be compared across diff --git a/tests/lib/direct_url.py b/tests/lib/direct_url.py deleted file mode 100644 index 3049da9c1d2..00000000000 --- a/tests/lib/direct_url.py +++ /dev/null @@ -1,27 +0,0 @@ -from __future__ import annotations - -import os -import re -from pathlib import Path - -from pip._internal.models.direct_url import DIRECT_URL_METADATA_NAME, DirectUrl - -from tests.lib import TestPipResult - - -def get_created_direct_url_path(result: TestPipResult, pkg: str) -> Path | None: - direct_url_metadata_re = re.compile( - pkg + r"-[\d\.]+\.dist-info." + DIRECT_URL_METADATA_NAME + r"$" - ) - for filename in result.files_created: - if direct_url_metadata_re.search(os.fspath(filename)): - return result.test_env.base_path / filename - return None - - -def get_created_direct_url(result: TestPipResult, pkg: str) -> DirectUrl | None: - direct_url_path = get_created_direct_url_path(result, pkg) - if direct_url_path: - with open(direct_url_path) as f: - return DirectUrl.from_json(f.read()) - return None diff --git a/tests/unit/test_wheel_builder.py b/tests/unit/test_wheel_builder.py index 0547ac818bc..f29e75a48c8 100644 --- a/tests/unit/test_wheel_builder.py +++ b/tests/unit/test_wheel_builder.py @@ -47,43 +47,6 @@ class ReqMock: supports_pyproject_editable: bool = False -@pytest.mark.parametrize( - "req, expected", - [ - # We build, whether pep 517 is enabled or not. - (ReqMock(use_pep517=True), True), - (ReqMock(use_pep517=False), True), - # We don't build reqs that are already wheels. - (ReqMock(is_wheel=True), False), - # We build editables if the backend supports PEP 660. - (ReqMock(editable=True, use_pep517=False), False), - ( - ReqMock(editable=True, use_pep517=True, supports_pyproject_editable=True), - True, - ), - ( - ReqMock(editable=True, use_pep517=True, supports_pyproject_editable=False), - False, - ), - # By default (i.e. when binaries are allowed), VCS requirements - # should be built in install mode. - ( - ReqMock(link=Link("git+https://g.c/org/repo"), use_pep517=True), - True, - ), - ( - ReqMock(link=Link("git+https://g.c/org/repo"), use_pep517=False), - True, - ), - ], -) -def test_should_build_for_install_command(req: ReqMock, expected: bool) -> None: - should_build = wheel_builder.should_build_for_install_command( - cast(InstallRequirement, req), - ) - assert should_build is expected - - @pytest.mark.parametrize( "req, expected", [