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

Build local directories in-place with feature flag #9091

Merged
merged 1 commit into from
Feb 23, 2021
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
10 changes: 9 additions & 1 deletion docs/html/reference/pip_install.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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`:
Expand Down
4 changes: 4 additions & 0 deletions news/9091.feature.rst
Original file line number Diff line number Diff line change
@@ -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 <https://pip.pypa.io/en/stable/user_guide/#installing-from-local-packages>`_
for more information.
2 changes: 1 addition & 1 deletion src/pip/_internal/cli/cmdoptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -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]

Expand Down
1 change: 1 addition & 0 deletions src/pip/_internal/cli/req_command.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
34 changes: 31 additions & 3 deletions src/pip/_internal/operations/prepare.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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__()
Expand Down Expand Up @@ -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]]

Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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(
Expand Down
22 changes: 22 additions & 0 deletions tests/functional/test_install.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions tests/unit/test_req.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down