diff -r 01824bf55376 Doc/library/shutil.rst --- a/Doc/library/shutil.rst Fri May 04 11:06:09 2012 -0400 +++ b/Doc/library/shutil.rst Sat May 05 22:11:25 2012 +0200 @@ -102,14 +102,14 @@ .. 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 -r 01824bf55376 Lib/shutil.py --- a/Lib/shutil.py Fri May 04 11:06:09 2012 -0400 +++ b/Lib/shutil.py Sat May 05 22:11:25 2012 +0200 @@ -164,6 +164,36 @@ why.errno != errno.EOPNOTSUPP): 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"). @@ -191,6 +221,7 @@ 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 -r 01824bf55376 Lib/test/support.py --- a/Lib/test/support.py Fri May 04 11:06:09 2012 -0400 +++ b/Lib/test/support.py Sat May 05 22:11:25 2012 +0200 @@ -1696,6 +1696,35 @@ 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 -r 01824bf55376 Lib/test/test_os.py --- a/Lib/test/test_os.py Fri May 04 11:06:09 2012 -0400 +++ b/Lib/test/test_os.py Sat May 05 22:11:25 2012 +0200 @@ -1810,25 +1810,7 @@ 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 -r 01824bf55376 Lib/test/test_shutil.py --- a/Lib/test/test_shutil.py Fri May 04 11:06:09 2012 -0400 +++ b/Lib/test/test_shutil.py Sat May 05 22:11:25 2012 +0200 @@ -282,6 +282,67 @@ self.assertTrue(abs(os.stat(src).st_mtime - os.stat(dst).st_mtime) < 00000.1) + @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() @@ -340,6 +401,19 @@ 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()