diff --git a/docs/html/reference/pip_install.rst b/docs/html/reference/pip_install.rst index 81e315ebaa2..742c4ddb3c6 100644 --- a/docs/html/reference/pip_install.rst +++ b/docs/html/reference/pip_install.rst @@ -808,7 +808,15 @@ You can install local projects by specifying the project path to pip: During regular installation, pip will copy the entire project directory to a temporary location and install from there. The exception is that pip will exclude .tox and .nox directories present in the top level of the project from -being copied. +being copied. This approach is the cause of several performance and correctness +issues, so it is planned that pip 21.3 will change to install directly from the +local project directory. Depending on the build backend used by the project, +this may generate secondary build artifacts in the project directory, such as +the ``.egg-info`` and ``build`` directories in the case of the setuptools +backend. + +To opt in to the future behavior, specify the ``--use-feature=in-tree-build`` +option in pip's command line. .. _`editable-installs`: diff --git a/news/9091.feature.rst b/news/9091.feature.rst new file mode 100644 index 00000000000..8147e79c5e8 --- /dev/null +++ b/news/9091.feature.rst @@ -0,0 +1,4 @@ +Add a feature ``--use-feature=in-tree-build`` to build local projects in-place +when installing. This is expected to become the default behavior in pip 21.3; +see `Installing from local packages `_ +for more information. diff --git a/src/pip/_internal/cli/cmdoptions.py b/src/pip/_internal/cli/cmdoptions.py index 3075de94e39..7dc3d30571f 100644 --- a/src/pip/_internal/cli/cmdoptions.py +++ b/src/pip/_internal/cli/cmdoptions.py @@ -951,7 +951,7 @@ def check_list_path_option(options): metavar="feature", action="append", default=[], - choices=["2020-resolver", "fast-deps"], + choices=["2020-resolver", "fast-deps", "in-tree-build"], help="Enable new functionality, that may be backward incompatible.", ) # type: Callable[..., Option] diff --git a/src/pip/_internal/cli/req_command.py b/src/pip/_internal/cli/req_command.py index 4302b5bdc8d..a55dd7516d8 100644 --- a/src/pip/_internal/cli/req_command.py +++ b/src/pip/_internal/cli/req_command.py @@ -245,6 +245,7 @@ def make_requirement_preparer( require_hashes=options.require_hashes, use_user_site=use_user_site, lazy_wheel=lazy_wheel, + in_tree_build="in-tree-build" in options.features_enabled, ) @classmethod diff --git a/src/pip/_internal/operations/prepare.py b/src/pip/_internal/operations/prepare.py index 72743648a7e..3d074f9f629 100644 --- a/src/pip/_internal/operations/prepare.py +++ b/src/pip/_internal/operations/prepare.py @@ -35,6 +35,7 @@ from pip._internal.network.session import PipSession from pip._internal.req.req_install import InstallRequirement from pip._internal.req.req_tracker import RequirementTracker +from pip._internal.utils.deprecation import deprecated from pip._internal.utils.filesystem import copy2_fixed from pip._internal.utils.hashes import Hashes, MissingHashes from pip._internal.utils.logging import indent_log @@ -207,8 +208,23 @@ def unpack_url( unpack_vcs_link(link, location) return None - # If it's a url to a local directory + # Once out-of-tree-builds are no longer supported, could potentially + # replace the below condition with `assert not link.is_existing_dir` + # - unpack_url does not need to be called for in-tree-builds. + # + # As further cleanup, _copy_source_tree and accompanying tests can + # be removed. if link.is_existing_dir(): + deprecated( + "A future pip version will change local packages to be built " + "in-place without first copying to a temporary directory. " + "We recommend you use --use-feature=in-tree-build to test " + "your packages with this new behavior before it becomes the " + "default.\n", + replacement=None, + gone_in="21.3", + issue=7555 + ) if os.path.isdir(location): rmtree(location) _copy_source_tree(link.file_path, location) @@ -278,6 +294,7 @@ def __init__( require_hashes, # type: bool use_user_site, # type: bool lazy_wheel, # type: bool + in_tree_build, # type: bool ): # type: (...) -> None super().__init__() @@ -306,6 +323,9 @@ def __init__( # Should wheels be downloaded lazily? self.use_lazy_wheel = lazy_wheel + # Should in-tree builds be used for local paths? + self.in_tree_build = in_tree_build + # Memoized downloaded files, as mapping of url: (path, mime type) self._downloaded = {} # type: Dict[str, Tuple[str, str]] @@ -339,6 +359,11 @@ def _ensure_link_req_src_dir(self, req, parallel_builds): # directory. return assert req.source_dir is None + if req.link.is_existing_dir() and self.in_tree_build: + # build local directories in-tree + req.source_dir = req.link.file_path + return + # We always delete unpacked sdists after pip runs. req.ensure_has_source_dir( self.build_dir, @@ -517,11 +542,14 @@ def _prepare_linked_requirement(self, req, parallel_builds): self._ensure_link_req_src_dir(req, parallel_builds) hashes = self._get_linked_req_hashes(req) - if link.url not in self._downloaded: + + if link.is_existing_dir() and self.in_tree_build: + local_file = None + elif link.url not in self._downloaded: try: local_file = unpack_url( link, req.source_dir, self._download, - self.download_dir, hashes, + self.download_dir, hashes ) except NetworkConnectionError as exc: raise InstallationError( diff --git a/tests/functional/test_install.py b/tests/functional/test_install.py index 44342978e74..0b33afeac39 100644 --- a/tests/functional/test_install.py +++ b/tests/functional/test_install.py @@ -581,6 +581,28 @@ def test_install_from_local_directory_with_symlinks_to_directories( result.did_create(dist_info_folder) +def test_install_from_local_directory_with_in_tree_build( + script, data, with_wheel +): + """ + Test installing from a local directory with --use-feature=in-tree-build. + """ + to_install = data.packages.joinpath("FSPkg") + args = ["install", "--use-feature=in-tree-build", to_install] + + in_tree_build_dir = to_install / "build" + assert not in_tree_build_dir.exists() + result = script.pip(*args) + fspkg_folder = script.site_packages / 'fspkg' + dist_info_folder = ( + script.site_packages / + 'FSPkg-0.1.dev0.dist-info' + ) + result.did_create(fspkg_folder) + result.did_create(dist_info_folder) + assert in_tree_build_dir.exists() + + @pytest.mark.skipif("sys.platform == 'win32' or sys.version_info < (3,)") def test_install_from_local_directory_with_socket_file( script, data, tmpdir, with_wheel diff --git a/tests/unit/test_req.py b/tests/unit/test_req.py index 9eab6fab04a..5f01a9ecc23 100644 --- a/tests/unit/test_req.py +++ b/tests/unit/test_req.py @@ -89,6 +89,7 @@ def _basic_resolver(self, finder, require_hashes=False): require_hashes=require_hashes, use_user_site=False, lazy_wheel=False, + in_tree_build=False, ) yield Resolver( preparer=preparer,