summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--Doc/library/shutil.rst6
-rw-r--r--Lib/shutil.py31
-rw-r--r--Lib/test/support.py29
-rw-r--r--Lib/test/test_os.py20
-rw-r--r--Lib/test/test_shutil.py74
-rw-r--r--Misc/NEWS3
6 files changed, 141 insertions, 22 deletions
diff --git a/Doc/library/shutil.rst b/Doc/library/shutil.rst
index 21ee94f..3b5c1bd 100644
--- a/Doc/library/shutil.rst
+++ b/Doc/library/shutil.rst
@@ -102,14 +102,14 @@ Directory and files operations
.. function:: copy2(src, dst[, symlinks=False])
- Similar to :func:`shutil.copy`, but metadata is copied as well -- in fact,
- this is just :func:`shutil.copy` followed by :func:`copystat`. This is
+ Similar to :func:`shutil.copy`, but metadata is copied as well. This is
similar to the Unix command :program:`cp -p`. If *symlinks* is true,
symbolic links won't be followed but recreated instead -- this resembles
GNU's :program:`cp -P`.
.. versionchanged:: 3.3
- Added *symlinks* argument.
+ Added *symlinks* argument, try to copy extended file system attributes
+ too (currently Linux only).
.. function:: ignore_patterns(\*patterns)
diff --git a/Lib/shutil.py b/Lib/shutil.py
index 9625d36..ce60c3b 100644
--- a/Lib/shutil.py
+++ b/Lib/shutil.py
@@ -166,6 +166,36 @@ def copystat(src, dst, symlinks=False):
else:
raise
+if hasattr(os, 'listxattr'):
+ def _copyxattr(src, dst, symlinks=False):
+ """Copy extended filesystem attributes from `src` to `dst`.
+
+ Overwrite existing attributes.
+
+ If the optional flag `symlinks` is set, symlinks won't be followed.
+
+ """
+ if symlinks:
+ listxattr = os.llistxattr
+ removexattr = os.lremovexattr
+ setxattr = os.lsetxattr
+ getxattr = os.lgetxattr
+ else:
+ listxattr = os.listxattr
+ removexattr = os.removexattr
+ setxattr = os.setxattr
+ getxattr = os.getxattr
+
+ for attr in listxattr(src):
+ try:
+ setxattr(dst, attr, getxattr(src, attr))
+ except OSError as e:
+ if e.errno not in (errno.EPERM, errno.ENOTSUP, errno.ENODATA):
+ raise
+else:
+ def _copyxattr(*args, **kwargs):
+ pass
+
def copy(src, dst, symlinks=False):
"""Copy data and mode bits ("cp src dst").
@@ -193,6 +223,7 @@ def copy2(src, dst, symlinks=False):
dst = os.path.join(dst, os.path.basename(src))
copyfile(src, dst, symlinks=symlinks)
copystat(src, dst, symlinks=symlinks)
+ _copyxattr(src, dst, symlinks=symlinks)
def ignore_patterns(*patterns):
"""Function that can be used as copytree() ignore parameter.
diff --git a/Lib/test/support.py b/Lib/test/support.py
index 48e8332..c92fa00 100644
--- a/Lib/test/support.py
+++ b/Lib/test/support.py
@@ -1696,6 +1696,35 @@ def skip_unless_symlink(test):
msg = "Requires functional symlink implementation"
return test if ok else unittest.skip(msg)(test)
+_can_xattr = None
+def can_xattr():
+ global _can_xattr
+ if _can_xattr is not None:
+ return _can_xattr
+ if not hasattr(os, "setxattr"):
+ can = False
+ else:
+ try:
+ with open(TESTFN, "wb") as fp:
+ try:
+ os.fsetxattr(fp.fileno(), b"user.test", b"")
+ # Kernels < 2.6.39 don't respect setxattr flags.
+ kernel_version = platform.release()
+ m = re.match("2.6.(\d{1,2})", kernel_version)
+ can = m is None or int(m.group(1)) >= 39
+ except OSError:
+ can = False
+ finally:
+ unlink(TESTFN)
+ _can_xattr = can
+ return can
+
+def skip_unless_xattr(test):
+ """Skip decorator for tests that require functional extended attributes"""
+ ok = can_xattr()
+ msg = "no non-broken extended attribute support"
+ return test if ok else unittest.skip(msg)(test)
+
def patch(test_instance, object_to_patch, attr_name, new_value):
"""Override 'object_to_patch'.'attr_name' with 'new_value'.
diff --git a/Lib/test/test_os.py b/Lib/test/test_os.py
index 0c15f22..066bf72 100644
--- a/Lib/test/test_os.py
+++ b/Lib/test/test_os.py
@@ -1810,25 +1810,7 @@ class TestSendfile(unittest.TestCase):
raise
-def supports_extended_attributes():
- if not hasattr(os, "setxattr"):
- return False
- try:
- with open(support.TESTFN, "wb") as fp:
- try:
- os.fsetxattr(fp.fileno(), b"user.test", b"")
- except OSError:
- return False
- finally:
- support.unlink(support.TESTFN)
- # Kernels < 2.6.39 don't respect setxattr flags.
- kernel_version = platform.release()
- m = re.match("2.6.(\d{1,2})", kernel_version)
- return m is None or int(m.group(1)) >= 39
-
-
-@unittest.skipUnless(supports_extended_attributes(),
- "no non-broken extended attribute support")
+@support.skip_unless_xattr
class ExtendedAttributeTests(unittest.TestCase):
def tearDown(self):
diff --git a/Lib/test/test_shutil.py b/Lib/test/test_shutil.py
index 4700a49..59d83e8 100644
--- a/Lib/test/test_shutil.py
+++ b/Lib/test/test_shutil.py
@@ -311,6 +311,67 @@ class TestShutil(unittest.TestCase):
finally:
os.chflags = old_chflags
+ @support.skip_unless_xattr
+ def test_copyxattr(self):
+ tmp_dir = self.mkdtemp()
+ src = os.path.join(tmp_dir, 'foo')
+ write_file(src, 'foo')
+ dst = os.path.join(tmp_dir, 'bar')
+ write_file(dst, 'bar')
+
+ # no xattr == no problem
+ shutil._copyxattr(src, dst)
+ # common case
+ os.setxattr(src, 'user.foo', b'42')
+ os.setxattr(src, 'user.bar', b'43')
+ shutil._copyxattr(src, dst)
+ self.assertEqual(os.listxattr(src), os.listxattr(dst))
+ self.assertEqual(
+ os.getxattr(src, 'user.foo'),
+ os.getxattr(dst, 'user.foo'))
+ # check errors don't affect other attrs
+ os.remove(dst)
+ write_file(dst, 'bar')
+ os_error = OSError(errno.EPERM, 'EPERM')
+
+ def _raise_on_user_foo(fname, attr, val):
+ if attr == 'user.foo':
+ raise os_error
+ else:
+ orig_setxattr(fname, attr, val)
+ try:
+ orig_setxattr = os.setxattr
+ os.setxattr = _raise_on_user_foo
+ shutil._copyxattr(src, dst)
+ self.assertEqual(['user.bar'], os.listxattr(dst))
+ finally:
+ os.setxattr = orig_setxattr
+
+ @support.skip_unless_symlink
+ @support.skip_unless_xattr
+ @unittest.skipUnless(hasattr(os, 'geteuid') and os.geteuid() == 0,
+ 'root privileges required')
+ def test_copyxattr_symlinks(self):
+ # On Linux, it's only possible to access non-user xattr for symlinks;
+ # which in turn require root privileges. This test should be expanded
+ # as soon as other platforms gain support for extended attributes.
+ tmp_dir = self.mkdtemp()
+ src = os.path.join(tmp_dir, 'foo')
+ src_link = os.path.join(tmp_dir, 'baz')
+ write_file(src, 'foo')
+ os.symlink(src, src_link)
+ os.setxattr(src, 'trusted.foo', b'42')
+ os.lsetxattr(src_link, 'trusted.foo', b'43')
+ dst = os.path.join(tmp_dir, 'bar')
+ dst_link = os.path.join(tmp_dir, 'qux')
+ write_file(dst, 'bar')
+ os.symlink(dst, dst_link)
+ shutil._copyxattr(src_link, dst_link, symlinks=True)
+ self.assertEqual(os.lgetxattr(dst_link, 'trusted.foo'), b'43')
+ self.assertRaises(OSError, os.getxattr, dst, 'trusted.foo')
+ shutil._copyxattr(src_link, dst, symlinks=True)
+ self.assertEqual(os.getxattr(dst, 'trusted.foo'), b'43')
+
@support.skip_unless_symlink
def test_copy_symlinks(self):
tmp_dir = self.mkdtemp()
@@ -369,6 +430,19 @@ class TestShutil(unittest.TestCase):
if hasattr(os, 'lchflags') and hasattr(src_link_stat, 'st_flags'):
self.assertEqual(src_link_stat.st_flags, dst_stat.st_flags)
+ @support.skip_unless_xattr
+ def test_copy2_xattr(self):
+ tmp_dir = self.mkdtemp()
+ src = os.path.join(tmp_dir, 'foo')
+ dst = os.path.join(tmp_dir, 'bar')
+ write_file(src, 'foo')
+ os.setxattr(src, 'user.foo', b'42')
+ shutil.copy2(src, dst)
+ self.assertEqual(
+ os.getxattr(src, 'user.foo'),
+ os.getxattr(dst, 'user.foo'))
+ os.remove(dst)
+
@support.skip_unless_symlink
def test_copyfile_symlinks(self):
tmp_dir = self.mkdtemp()
diff --git a/Misc/NEWS b/Misc/NEWS
index 10cabbf..78a5ef9 100644
--- a/Misc/NEWS
+++ b/Misc/NEWS
@@ -23,6 +23,9 @@ Core and Builtins
Library
-------
+- Issue #14082: shutil.copy2() now copies extended attributes, if possible.
+ Patch by Hynek Schlawack.
+
- Issue #13959: Make importlib.abc.FileLoader.load_module()/get_filename() and
importlib.machinery.ExtensionFileLoader.load_module() have their single
argument be optional. Allows for the replacement (and thus deprecation) of