diff --git a/docs/html/reference/pip_install.rst b/docs/html/reference/pip_install.rst
index 96fd1c018ba..f56dc9a0e9f 100644
--- a/docs/html/reference/pip_install.rst
+++ b/docs/html/reference/pip_install.rst
@@ -694,10 +694,21 @@ does not satisfy the ``--require-hashes`` demand that every package have a
local hash.
+Local project installs
+++++++++++++++++++++++
+pip supports installing local project in both regular mode and editable mode.
+You can install local projects by specifying the project path to pip::
+
+$ pip install path/to/SomeProject
+
+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.
+
+
.. _`editable-installs`:
"Editable" Installs
-+++++++++++++++++++
+~~~~~~~~~~~~~~~~~~~
"Editable" installs are fundamentally `"setuptools develop mode"
`_
diff --git a/news/6770.bugfix b/news/6770.bugfix
new file mode 100644
index 00000000000..c0ab57ee109
--- /dev/null
+++ b/news/6770.bugfix
@@ -0,0 +1 @@
+Skip copying .tox and .nox directories to temporary build directories
diff --git a/src/pip/_internal/download.py b/src/pip/_internal/download.py
index 8715eb5b19d..95963a1d2d5 100644
--- a/src/pip/_internal/download.py
+++ b/src/pip/_internal/download.py
@@ -934,12 +934,23 @@ def unpack_file_url(
of the link file inside download_dir.
"""
link_path = url_to_path(link.url_without_fragment)
-
# If it's a url to a local directory
if is_dir_url(link):
+
+ def ignore(d, names):
+ # Pulling in those directories can potentially be very slow,
+ # exclude the following directories if they appear in the top
+ # level dir (and only it).
+ # See discussion at https://github.com/pypa/pip/pull/6770
+ return ['.tox', '.nox'] if d == link_path else []
+
if os.path.isdir(location):
rmtree(location)
- shutil.copytree(link_path, location, symlinks=True)
+ shutil.copytree(link_path,
+ location,
+ symlinks=True,
+ ignore=ignore)
+
if download_dir:
logger.info('Link is a directory, ignoring download_dir')
return
diff --git a/tests/unit/test_download.py b/tests/unit/test_download.py
index 7b421a7d7d5..afc32e29dca 100644
--- a/tests/unit/test_download.py
+++ b/tests/unit/test_download.py
@@ -413,6 +413,41 @@ def test_unpack_file_url_thats_a_dir(self, tmpdir, data):
assert os.path.isdir(os.path.join(self.build_dir, 'fspkg'))
+@pytest.mark.parametrize('exclude_dir', [
+ '.nox',
+ '.tox'
+])
+def test_unpack_file_url_excludes_expected_dirs(tmpdir, exclude_dir):
+ src_dir = tmpdir / 'src'
+ dst_dir = tmpdir / 'dst'
+ src_included_file = src_dir.joinpath('file.txt')
+ src_excluded_dir = src_dir.joinpath(exclude_dir)
+ src_excluded_file = src_dir.joinpath(exclude_dir, 'file.txt')
+ src_included_dir = src_dir.joinpath('subdir', exclude_dir)
+
+ # set up source directory
+ src_excluded_dir.mkdir(parents=True)
+ src_included_dir.mkdir(parents=True)
+ src_included_file.touch()
+ src_excluded_file.touch()
+
+ dst_included_file = dst_dir.joinpath('file.txt')
+ dst_excluded_dir = dst_dir.joinpath(exclude_dir)
+ dst_excluded_file = dst_dir.joinpath(exclude_dir, 'file.txt')
+ dst_included_dir = dst_dir.joinpath('subdir', exclude_dir)
+
+ src_link = Link(path_to_url(src_dir))
+ unpack_file_url(
+ src_link,
+ dst_dir,
+ download_dir=None
+ )
+ assert not os.path.isdir(dst_excluded_dir)
+ assert not os.path.isfile(dst_excluded_file)
+ assert os.path.isfile(dst_included_file)
+ assert os.path.isdir(dst_included_dir)
+
+
class TestSafeFileCache:
"""
The no_perms test are useless on Windows since SafeFileCache uses