Skip to content

Commit

Permalink
bpo-26826: Expose copy_file_range in the os module (pythonGH-7255)
Browse files Browse the repository at this point in the history
  • Loading branch information
pablogsal authored May 31, 2019
1 parent 545a3b8 commit aac4d03
Show file tree
Hide file tree
Showing 9 changed files with 363 additions and 19 deletions.
22 changes: 22 additions & 0 deletions Doc/library/os.rst
Original file line number Diff line number Diff line change
Expand Up @@ -707,6 +707,28 @@ as internal buffering of data.
pass


.. function:: copy_file_range(src, dst, count, offset_src=None, offset_dst=None)

Copy *count* bytes from file descriptor *src*, starting from offset
*offset_src*, to file descriptor *dst*, starting from offset *offset_dst*.
If *offset_src* is None, then *src* is read from the current position;
respectively for *offset_dst*. The files pointed by *src* and *dst*
must reside in the same filesystem, otherwise an :exc:`OSError` is
raised with :attr:`~OSError.errno` set to :data:`errno.EXDEV`.

This copy is done without the additional cost of transferring data
from the kernel to user space and then back into the kernel. Additionally,
some filesystems could implement extra optimizations. The copy is done as if
both files are opened as binary.

The return value is the amount of bytes copied. This could be less than the
amount requested.

.. availability:: Linux kernel >= 4.5 or glibc >= 2.27.

.. versionadded:: 3.8


.. function:: device_encoding(fd)

Return a string describing the encoding of the device associated with *fd*
Expand Down
83 changes: 83 additions & 0 deletions Lib/test/test_os.py
Original file line number Diff line number Diff line change
Expand Up @@ -231,6 +231,89 @@ def test_symlink_keywords(self):
except (NotImplementedError, OSError):
pass # No OS support or unprivileged user

@unittest.skipUnless(hasattr(os, 'copy_file_range'), 'test needs os.copy_file_range()')
def test_copy_file_range_invalid_values(self):
with self.assertRaises(ValueError):
os.copy_file_range(0, 1, -10)

@unittest.skipUnless(hasattr(os, 'copy_file_range'), 'test needs os.copy_file_range()')
def test_copy_file_range(self):
TESTFN2 = support.TESTFN + ".3"
data = b'0123456789'

create_file(support.TESTFN, data)
self.addCleanup(support.unlink, support.TESTFN)

in_file = open(support.TESTFN, 'rb')
self.addCleanup(in_file.close)
in_fd = in_file.fileno()

out_file = open(TESTFN2, 'w+b')
self.addCleanup(support.unlink, TESTFN2)
self.addCleanup(out_file.close)
out_fd = out_file.fileno()

try:
i = os.copy_file_range(in_fd, out_fd, 5)
except OSError as e:
# Handle the case in which Python was compiled
# in a system with the syscall but without support
# in the kernel.
if e.errno != errno.ENOSYS:
raise
self.skipTest(e)
else:
# The number of copied bytes can be less than
# the number of bytes originally requested.
self.assertIn(i, range(0, 6));

with open(TESTFN2, 'rb') as in_file:
self.assertEqual(in_file.read(), data[:i])

@unittest.skipUnless(hasattr(os, 'copy_file_range'), 'test needs os.copy_file_range()')
def test_copy_file_range_offset(self):
TESTFN4 = support.TESTFN + ".4"
data = b'0123456789'
bytes_to_copy = 6
in_skip = 3
out_seek = 5

create_file(support.TESTFN, data)
self.addCleanup(support.unlink, support.TESTFN)

in_file = open(support.TESTFN, 'rb')
self.addCleanup(in_file.close)
in_fd = in_file.fileno()

out_file = open(TESTFN4, 'w+b')
self.addCleanup(support.unlink, TESTFN4)
self.addCleanup(out_file.close)
out_fd = out_file.fileno()

try:
i = os.copy_file_range(in_fd, out_fd, bytes_to_copy,
offset_src=in_skip,
offset_dst=out_seek)
except OSError as e:
# Handle the case in which Python was compiled
# in a system with the syscall but without support
# in the kernel.
if e.errno != errno.ENOSYS:
raise
self.skipTest(e)
else:
# The number of copied bytes can be less than
# the number of bytes originally requested.
self.assertIn(i, range(0, bytes_to_copy+1));

with open(TESTFN4, 'rb') as in_file:
read = in_file.read()
# seeked bytes (5) are zero'ed
self.assertEqual(read[:out_seek], b'\x00'*out_seek)
# 012 are skipped (in_skip)
# 345678 are copied in the file (in_skip + bytes_to_copy)
self.assertEqual(read[out_seek:],
data[in_skip:in_skip+i])

# Test attributes on return values from os.*stat* family.
class StatAttributeTests(unittest.TestCase):
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Expose :func:`copy_file_range` as a low level API in the :mod:`os` module.
108 changes: 107 additions & 1 deletion Modules/clinic/posixmodule.c.h

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

71 changes: 71 additions & 0 deletions Modules/posixmodule.c
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,10 @@ corresponding Unix manual entries for more information on calls.");
#include <sched.h>
#endif

#ifdef HAVE_COPY_FILE_RANGE
#include <unistd.h>
#endif

#if !defined(CPU_ALLOC) && defined(HAVE_SCHED_SETAFFINITY)
#undef HAVE_SCHED_SETAFFINITY
#endif
Expand Down Expand Up @@ -9455,8 +9459,74 @@ os_pwritev_impl(PyObject *module, int fd, PyObject *buffers, Py_off_t offset,
}
#endif /* HAVE_PWRITEV */

#ifdef HAVE_COPY_FILE_RANGE
/*[clinic input]
os.copy_file_range
src: int
Source file descriptor.
dst: int
Destination file descriptor.
count: Py_ssize_t
Number of bytes to copy.
offset_src: object = None
Starting offset in src.
offset_dst: object = None
Starting offset in dst.
Copy count bytes from one file descriptor to another.
If offset_src is None, then src is read from the current position;
respectively for offset_dst.
[clinic start generated code]*/

static PyObject *
os_copy_file_range_impl(PyObject *module, int src, int dst, Py_ssize_t count,
PyObject *offset_src, PyObject *offset_dst)
/*[clinic end generated code: output=1a91713a1d99fc7a input=42fdce72681b25a9]*/
{
off_t offset_src_val, offset_dst_val;
off_t *p_offset_src = NULL;
off_t *p_offset_dst = NULL;
Py_ssize_t ret;
int async_err = 0;
/* The flags argument is provided to allow
* for future extensions and currently must be to 0. */
int flags = 0;


if (count < 0) {
PyErr_SetString(PyExc_ValueError, "negative value for 'count' not allowed");
return NULL;
}

if (offset_src != Py_None) {
if (!Py_off_t_converter(offset_src, &offset_src_val)) {
return NULL;
}
p_offset_src = &offset_src_val;
}

if (offset_dst != Py_None) {
if (!Py_off_t_converter(offset_dst, &offset_dst_val)) {
return NULL;
}
p_offset_dst = &offset_dst_val;
}

do {
Py_BEGIN_ALLOW_THREADS
ret = copy_file_range(src, p_offset_src, dst, p_offset_dst, count, flags);
Py_END_ALLOW_THREADS
} while (ret < 0 && errno == EINTR && !(async_err = PyErr_CheckSignals()));

if (ret < 0) {
return (!async_err) ? posix_error() : NULL;
}

return PyLong_FromSsize_t(ret);
}
#endif /* HAVE_COPY_FILE_RANGE*/

#ifdef HAVE_MKFIFO
/*[clinic input]
Expand Down Expand Up @@ -13432,6 +13502,7 @@ static PyMethodDef posix_methods[] = {
OS_POSIX_SPAWN_METHODDEF
OS_POSIX_SPAWNP_METHODDEF
OS_READLINK_METHODDEF
OS_COPY_FILE_RANGE_METHODDEF
OS_RENAME_METHODDEF
OS_REPLACE_METHODDEF
OS_RMDIR_METHODDEF
Expand Down
Loading

0 comments on commit aac4d03

Please sign in to comment.