Skip to content
Closed
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 Doc/library/shutil.rst
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,9 @@ Directory and files operations
*dst* and return *dst* in the most efficient way possible.
*src* and *dst* are path-like objects or path names given as strings.

When *src* is a named pipe or a Unix socket, :exc:`SpecialFileError`
is raised.

*dst* must be the complete target file name; look at :func:`~shutil.copy`
for a copy that accepts a target directory path. If *src* and *dst*
specify the same file, :exc:`SameFileError` is raised.
Expand Down Expand Up @@ -83,6 +86,11 @@ Directory and files operations
copy the file more efficiently. See
:ref:`shutil-platform-dependent-efficient-copy-operations` section.

.. versionchanged:: 3.12
Raise :exc:`~shutil.SpecialFileError` instead of :exc:`OSError` when
copying a Unix socket. Since the former is a subclass of the latter, this
change is backward compatible.

.. exception:: SameFileError

This exception is raised if source and destination in :func:`copyfile`
Expand Down Expand Up @@ -369,7 +377,7 @@ Directory and files operations
If *copy_function* is given, it must be a callable that takes two arguments
*src* and *dst*, and will be used to copy *src* to *dst* if
:func:`os.rename` cannot be used. If the source is a directory,
:func:`copytree` is called, passing it the :func:`copy_function`. The
:func:`copytree` is called, passing it the *copy_function*. The
default *copy_function* is :func:`copy2`. Using :func:`~shutil.copy` as the
*copy_function* allows the move to succeed when it is not possible to also
copy the metadata, at the expense of not copying any of the metadata.
Expand Down
5 changes: 4 additions & 1 deletion Lib/shutil.py
Original file line number Diff line number Diff line change
Expand Up @@ -242,10 +242,13 @@ def copyfile(src, dst, *, follow_symlinks=True):
# File most likely does not exist
pass
else:
# XXX What about other special files? (sockets, devices...)
# XXX What about other special files? (devices...)
if stat.S_ISFIFO(st.st_mode):
fn = fn.path if isinstance(fn, os.DirEntry) else fn
raise SpecialFileError("`%s` is a named pipe" % fn)
elif stat.S_ISSOCK(st.st_mode):
fn = fn.path if isinstance(fn, os.DirEntry) else fn
raise SpecialFileError("`%s` is a socket" % fn)
if _WINDOWS and i == 0:
file_size = st.st_size

Expand Down
37 changes: 37 additions & 0 deletions Lib/test/test_shutil.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
import pathlib
import subprocess
import random
import socket
import string
import contextlib
import io
Expand Down Expand Up @@ -910,6 +911,28 @@ def test_copytree_named_pipe(self):
shutil.rmtree(TESTFN, ignore_errors=True)
shutil.rmtree(TESTFN2, ignore_errors=True)

@unittest.skipUnless(hasattr(socket, 'AF_UNIX'), 'requires socket.AF_UNIX')
def test_copytree_socket(self):

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IMO testing also copytree() is not very useful since internally it uses copyfile (which you are testing).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was copying what was done for named pipes for consistency. But having a test for copytree() makes sense to me because if in the future copytree() stopped using copyfile() for some reason, we would still want copytree() to raise an error?

os.mkdir(TESTFN)
self.addCleanup(shutil.rmtree, TESTFN, ignore_errors=True)
self.addCleanup(shutil.rmtree, TESTFN2, ignore_errors=True)
sock_path = tempfile.mktemp(dir=TESTFN)

sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
self.addCleanup(sock.close)
support.bind_unix_socket(sock, sock_path)
self.addCleanup(support.unlink, sock_path)

try:
shutil.copytree(TESTFN, TESTFN2)
except shutil.Error as e:
errors = e.args[0]
self.assertEqual(len(errors), 1)
src, dst, error_msg = errors[0]
self.assertEqual("`%s` is a socket" % sock_path, error_msg)
else:
self.fail("shutil.Error should have been raised")

def test_copytree_special_func(self):
src_dir = self.mkdtemp()
dst_dir = os.path.join(self.mkdtemp(), 'destination')
Expand Down Expand Up @@ -1453,6 +1476,20 @@ def test_copyfile_named_pipe(self):
finally:
os.remove(TESTFN)

@unittest.skipUnless(hasattr(socket, 'AF_UNIX'), 'requires socket.AF_UNIX')
def test_copyfile_socket(self):
tmp_dir = self.mkdtemp()
src = tempfile.mktemp(dir=tmp_dir)
dst = os.path.join(tmp_dir, 'dst')

sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
self.addCleanup(sock.close)
support.bind_unix_socket(sock, src)
self.addCleanup(support.unlink, src)

self.assertRaises(shutil.SpecialFileError,
shutil.copyfile, src, dst)

def test_copyfile_return_value(self):
# copytree returns its destination path.
src_dir = self.mkdtemp()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
:func:`shutil.copyfile` always raises :exc:`shutil.SpecialFileError`
when trying to copy a Unix socket.