Skip to content

Commit b4d8b10

Browse files
committed
Address review comments.
1 parent 4b4a1bb commit b4d8b10

File tree

5 files changed

+105
-26
lines changed

5 files changed

+105
-26
lines changed

src/pip/_internal/download.py

+31-19
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@
3232
# Import ssl from compat so the initial import occurs in only one place.
3333
from pip._internal.utils.compat import HAS_TLS, ssl
3434
from pip._internal.utils.encoding import auto_decode
35-
from pip._internal.utils.filesystem import check_path_owner, is_socket
35+
from pip._internal.utils.filesystem import check_path_owner, copytree
3636
from pip._internal.utils.glibc import libc_ver
3737
from pip._internal.utils.marker_files import write_delete_marker_file
3838
from pip._internal.utils.misc import (
@@ -46,6 +46,7 @@
4646
display_path,
4747
format_size,
4848
get_installed_version,
49+
path_to_display,
4950
path_to_url,
5051
remove_auth_from_url,
5152
rmtree,
@@ -60,7 +61,7 @@
6061

6162
if MYPY_CHECK_RUNNING:
6263
from typing import (
63-
Dict, IO, List, Optional, Text, Tuple, Union
64+
Dict, IO, Optional, Text, Tuple, Union
6465
)
6566
from optparse import Values
6667
from pip._internal.models.link import Link
@@ -963,25 +964,36 @@ def ignore(d, names):
963964
# See discussion at https://github.com/pypa/pip/pull/6770
964965
return ['.tox', '.nox'] if d == source else []
965966

967+
def ignore_special_file_errors(error_details):
968+
# Copying special files is not supported, so we skip errors related to
969+
# them. This is a convenience to support users that may have tools
970+
# creating e.g. socket files in their source directory.
971+
src, dest, error = error_details
972+
973+
if not isinstance(error, shutil.SpecialFileError):
974+
# Then it is some other kind of error that we do want to report.
975+
return True
976+
977+
# SpecialFileError may be raised due to either the source or
978+
# destination. If the destination was the cause then we would actually
979+
# care, but since the destination directory is deleted prior to
980+
# copy we ignore all of them assuming it is caused by the source.
981+
logger.warning(
982+
"Ignoring special file error '%s' encountered copying %s to %s.",
983+
str(error),
984+
path_to_display(src),
985+
path_to_display(dest),
986+
)
987+
966988
try:
967-
shutil.copytree(source, target, ignore=ignore, symlinks=True)
989+
copytree(source, target, ignore=ignore, symlinks=True)
968990
except shutil.Error as e:
969-
errors = e.args[0] # type: List[Tuple[str, str, shutil.Error]]
970-
# Users may have locally-created socket files in the source
971-
# directory. Copying socket files is not supported, so we skip errors
972-
# related to them, for convenience.
973-
974-
# Copy list to avoid mutation while iterating.
975-
errors_copy = errors[:]
976-
for i, err in reversed(list(enumerate(errors_copy))):
977-
src, _dest, _error = err
978-
if is_socket(src):
979-
# Remove errors related to sockets to prevent distractions if
980-
# we end up re-raising.
981-
errors.pop(i)
982-
983-
if errors:
984-
raise
991+
errors = e.args[0]
992+
normal_file_errors = list(
993+
filter(ignore_special_file_errors, errors)
994+
)
995+
if normal_file_errors:
996+
raise shutil.Error(normal_file_errors)
985997

986998

987999
def unpack_file_url(

src/pip/_internal/utils/filesystem.py

+30
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import os
22
import os.path
3+
import shutil
34
import stat
45

56
from pip._internal.utils.compat import get_path_uid
@@ -31,6 +32,35 @@ def check_path_owner(path):
3132
return False # assume we don't own the path
3233

3334

35+
def copytree(*args, **kwargs):
36+
"""Wrap shutil.copytree() to map errors copying socket file to
37+
SpecialFileError.
38+
39+
See also https://bugs.python.org/issue37700.
40+
"""
41+
def to_correct_error(src, dest, error):
42+
for f in [src, dest]:
43+
try:
44+
if is_socket(f):
45+
new_error = shutil.SpecialFileError("`%s` is a socket" % f)
46+
return (src, dest, new_error)
47+
except OSError:
48+
# An error has already occurred. Another error here is not
49+
# a problem and we can ignore it.
50+
pass
51+
52+
return (src, dest, error)
53+
54+
try:
55+
shutil.copytree(*args, **kwargs)
56+
except shutil.Error as e:
57+
errors = e.args[0]
58+
new_errors = [
59+
to_correct_error(src, dest, error) for src, dest, error in errors
60+
]
61+
raise shutil.Error(new_errors)
62+
63+
3464
def is_socket(path):
3565
# type: (str) -> bool
3666
return stat.S_ISSOCK(os.lstat(path).st_mode)

tests/functional/test_install.py

+4-2
Original file line numberDiff line numberDiff line change
@@ -504,11 +504,13 @@ def test_install_from_local_directory_with_socket_file(script, data, tmpdir):
504504

505505
shutil.copytree(to_copy, to_install)
506506
# Socket file, should be ignored.
507-
make_socket_file(os.path.join(to_install, "example"))
507+
socket_file_path = os.path.join(to_install, "example")
508+
make_socket_file(socket_file_path)
508509

509-
result = script.pip("install", to_install, expect_error=False)
510+
result = script.pip("install", "--verbose", to_install, expect_error=False)
510511
assert package_folder in result.files_created, str(result.stdout)
511512
assert egg_info_file in result.files_created, str(result)
513+
assert str(socket_file_path) in result.stderr
512514

513515

514516
def test_install_from_local_directory_with_no_setup_py(script, data):

tests/unit/test_download.py

+11-4
Original file line numberDiff line numberDiff line change
@@ -363,16 +363,23 @@ def test_copy_source_tree(clean_project, tmpdir):
363363

364364

365365
@pytest.mark.skipif("sys.platform == 'win32'")
366-
def test_copy_source_tree_with_socket(clean_project, tmpdir):
366+
def test_copy_source_tree_with_socket(clean_project, tmpdir, caplog):
367367
target = tmpdir.joinpath("target")
368368
expected_files = get_filelist(clean_project)
369-
make_socket_file(clean_project.joinpath("aaa"))
369+
socket_path = str(clean_project.joinpath("aaa"))
370+
make_socket_file(socket_path)
370371

371372
_copy_source_tree(clean_project, target)
372373

373374
copied_files = get_filelist(target)
374375
assert expected_files == copied_files
375376

377+
# Warning should have been logged.
378+
assert len(caplog.records) == 1
379+
record = caplog.records[0]
380+
assert record.levelname == 'WARNING'
381+
assert socket_path in record.message
382+
376383

377384
@pytest.mark.skipif("sys.platform == 'win32'")
378385
def test_copy_source_tree_with_socket_fails_with_no_socket_error(
@@ -392,7 +399,7 @@ def test_copy_source_tree_with_socket_fails_with_no_socket_error(
392399
assert unreadable_file in errored_files
393400

394401
copied_files = get_filelist(target)
395-
# Even with errors, all files should have been copied.
402+
# All files without errors should have been copied.
396403
assert expected_files == copied_files
397404

398405

@@ -410,7 +417,7 @@ def test_copy_source_tree_with_unreadable_dir_fails(clean_project, tmpdir):
410417
assert unreadable_file in errored_files
411418

412419
copied_files = get_filelist(target)
413-
# Even with errors, all files should have been copied.
420+
# All files without errors should have been copied.
414421
assert expected_files == copied_files
415422

416423

tests/unit/test_utils_filesystem.py

+29-1
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
import os
2+
import shutil
23

34
import pytest
45

5-
from pip._internal.utils.filesystem import is_socket
6+
from pip._internal.utils.filesystem import copytree, is_socket
67

78
from ..lib.filesystem import make_socket_file
89
from ..lib.path import Path
@@ -39,3 +40,30 @@ def test_is_socket(create, result, tmpdir):
3940
create(target)
4041
assert os.path.lexists(target)
4142
assert is_socket(target) == result
43+
44+
45+
@pytest.mark.skipif("sys.platform == 'win32'")
46+
def test_copytree_maps_socket_errors(tmpdir):
47+
src_dir = tmpdir.joinpath("src")
48+
make_dir(src_dir)
49+
make_file(src_dir.joinpath("a"))
50+
socket_src = src_dir.joinpath("b")
51+
make_socket_file(socket_src)
52+
make_file(src_dir.joinpath("c"))
53+
54+
dest_dir = tmpdir.joinpath("dest")
55+
socket_dest = dest_dir.joinpath("b")
56+
57+
with pytest.raises(shutil.Error) as e:
58+
copytree(src_dir, dest_dir)
59+
60+
errors = e.value.args[0]
61+
assert len(errors) == 1
62+
src, dest, error = errors[0]
63+
assert src == str(socket_src)
64+
assert dest == str(socket_dest)
65+
assert isinstance(error, shutil.SpecialFileError)
66+
67+
assert dest_dir.joinpath("a").exists()
68+
assert not socket_dest.exists()
69+
assert dest_dir.joinpath("c").exists()

0 commit comments

Comments
 (0)