summaryrefslogtreecommitdiffstats
path: root/Lib
diff options
context:
space:
mode:
Diffstat (limited to 'Lib')
-rw-r--r--Lib/shutil.py48
-rw-r--r--Lib/test/test_os.py61
-rw-r--r--Lib/test/test_shutil.py69
-rw-r--r--Lib/test/test_tools/test_lll.py6
-rw-r--r--Lib/test/test_venv.py6
5 files changed, 159 insertions, 31 deletions
diff --git a/Lib/shutil.py b/Lib/shutil.py
index ab1a7d6..39f793b 100644
--- a/Lib/shutil.py
+++ b/Lib/shutil.py
@@ -452,7 +452,14 @@ def _copytree(entries, src, dst, symlinks, ignore, copy_function,
dstname = os.path.join(dst, srcentry.name)
srcobj = srcentry if use_srcentry else srcname
try:
- if srcentry.is_symlink():
+ is_symlink = srcentry.is_symlink()
+ if is_symlink and os.name == 'nt':
+ # Special check for directory junctions, which appear as
+ # symlinks but we want to recurse.
+ lstat = srcentry.stat(follow_symlinks=False)
+ if lstat.st_reparse_tag == stat.IO_REPARSE_TAG_MOUNT_POINT:
+ is_symlink = False
+ if is_symlink:
linkto = os.readlink(srcname)
if symlinks:
# We can't just leave it to `copy_function` because legacy
@@ -537,6 +544,37 @@ def copytree(src, dst, symlinks=False, ignore=None, copy_function=copy2,
ignore_dangling_symlinks=ignore_dangling_symlinks,
dirs_exist_ok=dirs_exist_ok)
+if hasattr(stat, 'FILE_ATTRIBUTE_REPARSE_POINT'):
+ # Special handling for directory junctions to make them behave like
+ # symlinks for shutil.rmtree, since in general they do not appear as
+ # regular links.
+ def _rmtree_isdir(entry):
+ try:
+ st = entry.stat(follow_symlinks=False)
+ return (stat.S_ISDIR(st.st_mode) and not
+ (st.st_file_attributes & stat.FILE_ATTRIBUTE_REPARSE_POINT
+ and st.st_reparse_tag == stat.IO_REPARSE_TAG_MOUNT_POINT))
+ except OSError:
+ return False
+
+ def _rmtree_islink(path):
+ try:
+ st = os.lstat(path)
+ return (stat.S_ISLNK(st.st_mode) or
+ (st.st_file_attributes & stat.FILE_ATTRIBUTE_REPARSE_POINT
+ and st.st_reparse_tag == stat.IO_REPARSE_TAG_MOUNT_POINT))
+ except OSError:
+ return False
+else:
+ def _rmtree_isdir(entry):
+ try:
+ return entry.is_dir(follow_symlinks=False)
+ except OSError:
+ return False
+
+ def _rmtree_islink(path):
+ return os.path.islink(path)
+
# version vulnerable to race conditions
def _rmtree_unsafe(path, onerror):
try:
@@ -547,11 +585,7 @@ def _rmtree_unsafe(path, onerror):
entries = []
for entry in entries:
fullname = entry.path
- try:
- is_dir = entry.is_dir(follow_symlinks=False)
- except OSError:
- is_dir = False
- if is_dir:
+ if _rmtree_isdir(entry):
try:
if entry.is_symlink():
# This can only happen if someone replaces
@@ -681,7 +715,7 @@ def rmtree(path, ignore_errors=False, onerror=None):
os.close(fd)
else:
try:
- if os.path.islink(path):
+ if _rmtree_islink(path):
# symlinks to directories are forbidden, see bug #1669
raise OSError("Cannot call rmtree on a symbolic link")
except OSError:
diff --git a/Lib/test/test_os.py b/Lib/test/test_os.py
index 15fd65b..ba9f5c3 100644
--- a/Lib/test/test_os.py
+++ b/Lib/test/test_os.py
@@ -8,6 +8,7 @@ import codecs
import contextlib
import decimal
import errno
+import fnmatch
import fractions
import itertools
import locale
@@ -2253,6 +2254,20 @@ class ReadlinkTests(unittest.TestCase):
filelinkb = os.fsencode(filelink)
filelinkb_target = os.fsencode(filelink_target)
+ def assertPathEqual(self, left, right):
+ left = os.path.normcase(left)
+ right = os.path.normcase(right)
+ if sys.platform == 'win32':
+ # Bad practice to blindly strip the prefix as it may be required to
+ # correctly refer to the file, but we're only comparing paths here.
+ has_prefix = lambda p: p.startswith(
+ b'\\\\?\\' if isinstance(p, bytes) else '\\\\?\\')
+ if has_prefix(left):
+ left = left[4:]
+ if has_prefix(right):
+ right = right[4:]
+ self.assertEqual(left, right)
+
def setUp(self):
self.assertTrue(os.path.exists(self.filelink_target))
self.assertTrue(os.path.exists(self.filelinkb_target))
@@ -2274,14 +2289,14 @@ class ReadlinkTests(unittest.TestCase):
os.symlink(self.filelink_target, self.filelink)
self.addCleanup(support.unlink, self.filelink)
filelink = FakePath(self.filelink)
- self.assertEqual(os.readlink(filelink), self.filelink_target)
+ self.assertPathEqual(os.readlink(filelink), self.filelink_target)
@support.skip_unless_symlink
def test_pathlike_bytes(self):
os.symlink(self.filelinkb_target, self.filelinkb)
self.addCleanup(support.unlink, self.filelinkb)
path = os.readlink(FakePath(self.filelinkb))
- self.assertEqual(path, self.filelinkb_target)
+ self.assertPathEqual(path, self.filelinkb_target)
self.assertIsInstance(path, bytes)
@support.skip_unless_symlink
@@ -2289,7 +2304,7 @@ class ReadlinkTests(unittest.TestCase):
os.symlink(self.filelinkb_target, self.filelinkb)
self.addCleanup(support.unlink, self.filelinkb)
path = os.readlink(self.filelinkb)
- self.assertEqual(path, self.filelinkb_target)
+ self.assertPathEqual(path, self.filelinkb_target)
self.assertIsInstance(path, bytes)
@@ -2348,16 +2363,12 @@ class Win32SymlinkTests(unittest.TestCase):
# was created with target_is_dir==True.
os.remove(self.missing_link)
- @unittest.skip("currently fails; consider for improvement")
def test_isdir_on_directory_link_to_missing_target(self):
self._create_missing_dir_link()
- # consider having isdir return true for directory links
- self.assertTrue(os.path.isdir(self.missing_link))
+ self.assertFalse(os.path.isdir(self.missing_link))
- @unittest.skip("currently fails; consider for improvement")
def test_rmdir_on_directory_link_to_missing_target(self):
self._create_missing_dir_link()
- # consider allowing rmdir to remove directory links
os.rmdir(self.missing_link)
def check_stat(self, link, target):
@@ -2453,6 +2464,24 @@ class Win32SymlinkTests(unittest.TestCase):
except OSError:
pass
+ def test_appexeclink(self):
+ root = os.path.expandvars(r'%LOCALAPPDATA%\Microsoft\WindowsApps')
+ aliases = [os.path.join(root, a)
+ for a in fnmatch.filter(os.listdir(root), '*.exe')]
+
+ for alias in aliases:
+ if support.verbose:
+ print()
+ print("Testing with", alias)
+ st = os.lstat(alias)
+ self.assertEqual(st, os.stat(alias))
+ self.assertFalse(stat.S_ISLNK(st.st_mode))
+ self.assertEqual(st.st_reparse_tag, stat.IO_REPARSE_TAG_APPEXECLINK)
+ # testing the first one we see is sufficient
+ break
+ else:
+ self.skipTest("test requires an app execution alias")
+
@unittest.skipUnless(sys.platform == "win32", "Win32 specific tests")
class Win32JunctionTests(unittest.TestCase):
junction = 'junctiontest'
@@ -2460,25 +2489,29 @@ class Win32JunctionTests(unittest.TestCase):
def setUp(self):
assert os.path.exists(self.junction_target)
- assert not os.path.exists(self.junction)
+ assert not os.path.lexists(self.junction)
def tearDown(self):
- if os.path.exists(self.junction):
- # os.rmdir delegates to Windows' RemoveDirectoryW,
- # which removes junction points safely.
- os.rmdir(self.junction)
+ if os.path.lexists(self.junction):
+ os.unlink(self.junction)
def test_create_junction(self):
_winapi.CreateJunction(self.junction_target, self.junction)
+ self.assertTrue(os.path.lexists(self.junction))
self.assertTrue(os.path.exists(self.junction))
self.assertTrue(os.path.isdir(self.junction))
+ self.assertNotEqual(os.stat(self.junction), os.lstat(self.junction))
+ self.assertEqual(os.stat(self.junction), os.stat(self.junction_target))
- # Junctions are not recognized as links.
+ # bpo-37834: Junctions are not recognized as links.
self.assertFalse(os.path.islink(self.junction))
+ self.assertEqual(os.path.normcase("\\\\?\\" + self.junction_target),
+ os.path.normcase(os.readlink(self.junction)))
def test_unlink_removes_junction(self):
_winapi.CreateJunction(self.junction_target, self.junction)
self.assertTrue(os.path.exists(self.junction))
+ self.assertTrue(os.path.lexists(self.junction))
os.unlink(self.junction)
self.assertFalse(os.path.exists(self.junction))
diff --git a/Lib/test/test_shutil.py b/Lib/test/test_shutil.py
index 88dc4d9..636e3bd 100644
--- a/Lib/test/test_shutil.py
+++ b/Lib/test/test_shutil.py
@@ -42,6 +42,11 @@ try:
except ImportError:
UID_GID_SUPPORT = False
+try:
+ import _winapi
+except ImportError:
+ _winapi = None
+
def _fake_rename(*args, **kwargs):
# Pretend the destination path is on a different filesystem.
raise OSError(getattr(errno, 'EXDEV', 18), "Invalid cross-device link")
@@ -226,6 +231,47 @@ class TestShutil(unittest.TestCase):
self.assertTrue(os.path.exists(dir3))
self.assertTrue(os.path.exists(file1))
+ @unittest.skipUnless(_winapi, 'only relevant on Windows')
+ def test_rmtree_fails_on_junctions(self):
+ tmp = self.mkdtemp()
+ dir_ = os.path.join(tmp, 'dir')
+ os.mkdir(dir_)
+ link = os.path.join(tmp, 'link')
+ _winapi.CreateJunction(dir_, link)
+ self.assertRaises(OSError, shutil.rmtree, link)
+ self.assertTrue(os.path.exists(dir_))
+ self.assertTrue(os.path.lexists(link))
+ errors = []
+ def onerror(*args):
+ errors.append(args)
+ shutil.rmtree(link, onerror=onerror)
+ self.assertEqual(len(errors), 1)
+ self.assertIs(errors[0][0], os.path.islink)
+ self.assertEqual(errors[0][1], link)
+ self.assertIsInstance(errors[0][2][1], OSError)
+
+ @unittest.skipUnless(_winapi, 'only relevant on Windows')
+ def test_rmtree_works_on_junctions(self):
+ tmp = self.mkdtemp()
+ dir1 = os.path.join(tmp, 'dir1')
+ dir2 = os.path.join(dir1, 'dir2')
+ dir3 = os.path.join(tmp, 'dir3')
+ for d in dir1, dir2, dir3:
+ os.mkdir(d)
+ file1 = os.path.join(tmp, 'file1')
+ write_file(file1, 'foo')
+ link1 = os.path.join(dir1, 'link1')
+ _winapi.CreateJunction(dir2, link1)
+ link2 = os.path.join(dir1, 'link2')
+ _winapi.CreateJunction(dir3, link2)
+ link3 = os.path.join(dir1, 'link3')
+ _winapi.CreateJunction(file1, link3)
+ # make sure junctions are removed but not followed
+ shutil.rmtree(dir1)
+ self.assertFalse(os.path.exists(dir1))
+ self.assertTrue(os.path.exists(dir3))
+ self.assertTrue(os.path.exists(file1))
+
def test_rmtree_errors(self):
# filename is guaranteed not to exist
filename = tempfile.mktemp()
@@ -754,8 +800,12 @@ class TestShutil(unittest.TestCase):
src_stat = os.lstat(src_link)
shutil.copytree(src_dir, dst_dir, symlinks=True)
self.assertTrue(os.path.islink(os.path.join(dst_dir, 'sub', 'link')))
- self.assertEqual(os.readlink(os.path.join(dst_dir, 'sub', 'link')),
- os.path.join(src_dir, 'file.txt'))
+ actual = os.readlink(os.path.join(dst_dir, 'sub', 'link'))
+ # Bad practice to blindly strip the prefix as it may be required to
+ # correctly refer to the file, but we're only comparing paths here.
+ if os.name == 'nt' and actual.startswith('\\\\?\\'):
+ actual = actual[4:]
+ self.assertEqual(actual, os.path.join(src_dir, 'file.txt'))
dst_stat = os.lstat(dst_link)
if hasattr(os, 'lchmod'):
self.assertEqual(dst_stat.st_mode, src_stat.st_mode)
@@ -886,7 +936,6 @@ class TestShutil(unittest.TestCase):
shutil.copytree(src, dst, copy_function=custom_cpfun)
self.assertEqual(len(flag), 1)
- @unittest.skipIf(os.name == 'nt', 'temporarily disabled on Windows')
@unittest.skipUnless(hasattr(os, 'link'), 'requires os.link')
def test_dont_copy_file_onto_link_to_itself(self):
# bug 851123.
@@ -941,6 +990,20 @@ class TestShutil(unittest.TestCase):
finally:
shutil.rmtree(TESTFN, ignore_errors=True)
+ @unittest.skipUnless(_winapi, 'only relevant on Windows')
+ def test_rmtree_on_junction(self):
+ os.mkdir(TESTFN)
+ try:
+ src = os.path.join(TESTFN, 'cheese')
+ dst = os.path.join(TESTFN, 'shop')
+ os.mkdir(src)
+ open(os.path.join(src, 'spam'), 'wb').close()
+ _winapi.CreateJunction(src, dst)
+ self.assertRaises(OSError, shutil.rmtree, dst)
+ shutil.rmtree(dst, ignore_errors=True)
+ finally:
+ shutil.rmtree(TESTFN, ignore_errors=True)
+
# Issue #3002: copyfile and copytree block indefinitely on named pipes
@unittest.skipUnless(hasattr(os, "mkfifo"), 'requires os.mkfifo()')
def test_copyfile_named_pipe(self):
diff --git a/Lib/test/test_tools/test_lll.py b/Lib/test/test_tools/test_lll.py
index f3fbe961..b01e218 100644
--- a/Lib/test/test_tools/test_lll.py
+++ b/Lib/test/test_tools/test_lll.py
@@ -1,6 +1,7 @@
"""Tests for the lll script in the Tools/script directory."""
import os
+import sys
import tempfile
from test import support
from test.test_tools import skip_if_missing, import_tool
@@ -26,12 +27,13 @@ class lllTests(unittest.TestCase):
with support.captured_stdout() as output:
self.lll.main([dir1, dir2])
+ prefix = '\\\\?\\' if os.name == 'nt' else ''
self.assertEqual(output.getvalue(),
f'{dir1}:\n'
- f'symlink -> {fn1}\n'
+ f'symlink -> {prefix}{fn1}\n'
f'\n'
f'{dir2}:\n'
- f'symlink -> {fn2}\n'
+ f'symlink -> {prefix}{fn2}\n'
)
diff --git a/Lib/test/test_venv.py b/Lib/test/test_venv.py
index 9724d9e..de93d95 100644
--- a/Lib/test/test_venv.py
+++ b/Lib/test/test_venv.py
@@ -394,11 +394,7 @@ class EnsurePipTest(BaseTest):
with open(os.devnull, "rb") as f:
self.assertEqual(f.read(), b"")
- # Issue #20541: os.path.exists('nul') is False on Windows
- if os.devnull.lower() == 'nul':
- self.assertFalse(os.path.exists(os.devnull))
- else:
- self.assertTrue(os.path.exists(os.devnull))
+ self.assertTrue(os.path.exists(os.devnull))
def do_test_with_pip(self, system_site_packages):
rmtree(self.env_dir)