Skip to content

Commit 0ef2e97

Browse files
frenzymadnesskwi-dkserhiy-storchaka
authored andcommitted
Combines Two fixes for tempfile.TemporaryDirectory: python@e9b51c0 python@02a9259 Co-authored-by: Søren Løvborg <sorenl@unity3d.com> Co-authored-by: Serhiy Storchaka <storchaka@gmail.com>
1 parent 6a1ff2f commit 0ef2e97

File tree

2 files changed

+199
-11
lines changed

2 files changed

+199
-11
lines changed

Lib/tempfile.py

Lines changed: 42 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -276,6 +276,23 @@ def _mkstemp_inner(dir, pre, suf, flags, output_type):
276276
"No usable temporary file name found")
277277

278278

279+
def _dont_follow_symlinks(func, path, *args):
280+
# Pass follow_symlinks=False, unless not supported on this platform.
281+
if func in _os.supports_follow_symlinks:
282+
func(path, *args, follow_symlinks=False)
283+
elif _os.name == 'nt' or not _os.path.islink(path):
284+
func(path, *args)
285+
286+
287+
def _resetperms(path):
288+
try:
289+
chflags = _os.chflags
290+
except AttributeError:
291+
pass
292+
else:
293+
_dont_follow_symlinks(chflags, path, 0)
294+
_dont_follow_symlinks(_os.chmod, path, 0o700)
295+
279296
# User visible interfaces.
280297

281298
def gettempprefix():
@@ -794,9 +811,32 @@ def __init__(self, suffix=None, prefix=None, dir=None):
794811
self, self._cleanup, self.name,
795812
warn_message="Implicitly cleaning up {!r}".format(self))
796813

814+
@classmethod
815+
def _rmtree(cls, name):
816+
def onerror(func, path, exc_info):
817+
if issubclass(exc_info[0], PermissionError):
818+
try:
819+
if path != name:
820+
_resetperms(_os.path.dirname(path))
821+
_resetperms(path)
822+
823+
try:
824+
_os.unlink(path)
825+
# PermissionError is raised on FreeBSD for directories
826+
except (IsADirectoryError, PermissionError):
827+
cls._rmtree(path)
828+
except FileNotFoundError:
829+
pass
830+
elif issubclass(exc_info[0], FileNotFoundError):
831+
pass
832+
else:
833+
raise
834+
835+
_shutil.rmtree(name, onerror=onerror)
836+
797837
@classmethod
798838
def _cleanup(cls, name, warn_message):
799-
_shutil.rmtree(name)
839+
cls._rmtree(name)
800840
_warnings.warn(warn_message, ResourceWarning)
801841

802842
def __repr__(self):
@@ -810,4 +850,4 @@ def __exit__(self, exc, value, tb):
810850

811851
def cleanup(self):
812852
if self._finalizer.detach():
813-
_shutil.rmtree(self.name)
853+
self._rmtree(self.name)

Lib/test/test_tempfile.py

Lines changed: 157 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1298,19 +1298,25 @@ def __exit__(self, *exc_info):
12981298
class TestTemporaryDirectory(BaseTestCase):
12991299
"""Test TemporaryDirectory()."""
13001300

1301-
def do_create(self, dir=None, pre="", suf="", recurse=1):
1301+
def do_create(self, dir=None, pre="", suf="", recurse=1, dirs=1, files=1):
13021302
if dir is None:
13031303
dir = tempfile.gettempdir()
13041304
tmp = tempfile.TemporaryDirectory(dir=dir, prefix=pre, suffix=suf)
13051305
self.nameCheck(tmp.name, dir, pre, suf)
1306-
# Create a subdirectory and some files
1307-
if recurse:
1308-
d1 = self.do_create(tmp.name, pre, suf, recurse-1)
1309-
d1.name = None
1310-
with open(os.path.join(tmp.name, "test.txt"), "wb") as f:
1311-
f.write(b"Hello world!")
1306+
self.do_create2(tmp.name, recurse, dirs, files)
13121307
return tmp
13131308

1309+
def do_create2(self, path, recurse=1, dirs=1, files=1):
1310+
# Create subdirectories and some files
1311+
if recurse:
1312+
for i in range(dirs):
1313+
name = os.path.join(path, "dir%d" % i)
1314+
os.mkdir(name)
1315+
self.do_create2(name, recurse-1, dirs, files)
1316+
for i in range(files):
1317+
with open(os.path.join(path, "test%d.txt" % i), "wb") as f:
1318+
f.write(b"Hello world!")
1319+
13141320
def test_mkdtemp_failure(self):
13151321
# Check no additional exception if mkdtemp fails
13161322
# Previously would raise AttributeError instead
@@ -1350,11 +1356,108 @@ def test_cleanup_with_symlink_to_a_directory(self):
13501356
"TemporaryDirectory %s exists after cleanup" % d1.name)
13511357
self.assertTrue(os.path.exists(d2.name),
13521358
"Directory pointed to by a symlink was deleted")
1353-
self.assertEqual(os.listdir(d2.name), ['test.txt'],
1359+
self.assertEqual(os.listdir(d2.name), ['test0.txt'],
13541360
"Contents of the directory pointed to by a symlink "
13551361
"were deleted")
13561362
d2.cleanup()
13571363

1364+
@support.skip_unless_symlink
1365+
def test_cleanup_with_symlink_modes(self):
1366+
# cleanup() should not follow symlinks when fixing mode bits (#91133)
1367+
with self.do_create(recurse=0) as d2:
1368+
file1 = os.path.join(d2, 'file1')
1369+
open(file1, 'wb').close()
1370+
dir1 = os.path.join(d2, 'dir1')
1371+
os.mkdir(dir1)
1372+
for mode in range(8):
1373+
mode <<= 6
1374+
with self.subTest(mode=format(mode, '03o')):
1375+
def test(target, target_is_directory):
1376+
d1 = self.do_create(recurse=0)
1377+
symlink = os.path.join(d1.name, 'symlink')
1378+
os.symlink(target, symlink,
1379+
target_is_directory=target_is_directory)
1380+
try:
1381+
os.chmod(symlink, mode, follow_symlinks=False)
1382+
except NotImplementedError:
1383+
pass
1384+
try:
1385+
os.chmod(symlink, mode)
1386+
except FileNotFoundError:
1387+
pass
1388+
os.chmod(d1.name, mode)
1389+
d1.cleanup()
1390+
self.assertFalse(os.path.exists(d1.name))
1391+
1392+
with self.subTest('nonexisting file'):
1393+
test('nonexisting', target_is_directory=False)
1394+
with self.subTest('nonexisting dir'):
1395+
test('nonexisting', target_is_directory=True)
1396+
1397+
with self.subTest('existing file'):
1398+
os.chmod(file1, mode)
1399+
old_mode = os.stat(file1).st_mode
1400+
test(file1, target_is_directory=False)
1401+
new_mode = os.stat(file1).st_mode
1402+
self.assertEqual(new_mode, old_mode,
1403+
'%03o != %03o' % (new_mode, old_mode))
1404+
1405+
with self.subTest('existing dir'):
1406+
os.chmod(dir1, mode)
1407+
old_mode = os.stat(dir1).st_mode
1408+
test(dir1, target_is_directory=True)
1409+
new_mode = os.stat(dir1).st_mode
1410+
self.assertEqual(new_mode, old_mode,
1411+
'%03o != %03o' % (new_mode, old_mode))
1412+
1413+
@unittest.skipUnless(hasattr(os, 'chflags'), 'requires os.chflags')
1414+
@support.skip_unless_symlink
1415+
def test_cleanup_with_symlink_flags(self):
1416+
# cleanup() should not follow symlinks when fixing flags (#91133)
1417+
flags = stat.UF_IMMUTABLE | stat.UF_NOUNLINK
1418+
self.check_flags(flags)
1419+
1420+
with self.do_create(recurse=0) as d2:
1421+
file1 = os.path.join(d2, 'file1')
1422+
open(file1, 'wb').close()
1423+
dir1 = os.path.join(d2, 'dir1')
1424+
os.mkdir(dir1)
1425+
def test(target, target_is_directory):
1426+
d1 = self.do_create(recurse=0)
1427+
symlink = os.path.join(d1.name, 'symlink')
1428+
os.symlink(target, symlink,
1429+
target_is_directory=target_is_directory)
1430+
try:
1431+
os.chflags(symlink, flags, follow_symlinks=False)
1432+
except NotImplementedError:
1433+
pass
1434+
try:
1435+
os.chflags(symlink, flags)
1436+
except FileNotFoundError:
1437+
pass
1438+
os.chflags(d1.name, flags)
1439+
d1.cleanup()
1440+
self.assertFalse(os.path.exists(d1.name))
1441+
1442+
with self.subTest('nonexisting file'):
1443+
test('nonexisting', target_is_directory=False)
1444+
with self.subTest('nonexisting dir'):
1445+
test('nonexisting', target_is_directory=True)
1446+
1447+
with self.subTest('existing file'):
1448+
os.chflags(file1, flags)
1449+
old_flags = os.stat(file1).st_flags
1450+
test(file1, target_is_directory=False)
1451+
new_flags = os.stat(file1).st_flags
1452+
self.assertEqual(new_flags, old_flags)
1453+
1454+
with self.subTest('existing dir'):
1455+
os.chflags(dir1, flags)
1456+
old_flags = os.stat(dir1).st_flags
1457+
test(dir1, target_is_directory=True)
1458+
new_flags = os.stat(dir1).st_flags
1459+
self.assertEqual(new_flags, old_flags)
1460+
13581461
@support.cpython_only
13591462
def test_del_on_collection(self):
13601463
# A TemporaryDirectory is deleted when garbage collected
@@ -1385,7 +1488,7 @@ def test_del_on_shutdown(self):
13851488
13861489
tmp2 = os.path.join(tmp.name, 'test_dir')
13871490
os.mkdir(tmp2)
1388-
with open(os.path.join(tmp2, "test.txt"), "w") as f:
1491+
with open(os.path.join(tmp2, "test0.txt"), "w") as f:
13891492
f.write("Hello world!")
13901493
13911494
{mod}.tmp = tmp
@@ -1453,6 +1556,51 @@ def test_context_manager(self):
14531556
self.assertEqual(name, d.name)
14541557
self.assertFalse(os.path.exists(name))
14551558

1559+
def test_modes(self):
1560+
for mode in range(8):
1561+
mode <<= 6
1562+
with self.subTest(mode=format(mode, '03o')):
1563+
d = self.do_create(recurse=3, dirs=2, files=2)
1564+
with d:
1565+
# Change files and directories mode recursively.
1566+
for root, dirs, files in os.walk(d.name, topdown=False):
1567+
for name in files:
1568+
os.chmod(os.path.join(root, name), mode)
1569+
os.chmod(root, mode)
1570+
d.cleanup()
1571+
self.assertFalse(os.path.exists(d.name))
1572+
1573+
def check_flags(self, flags):
1574+
# skip the test if these flags are not supported (ex: FreeBSD 13)
1575+
filename = support.TESTFN
1576+
try:
1577+
open(filename, "w").close()
1578+
try:
1579+
os.chflags(filename, flags)
1580+
except OSError as exc:
1581+
# "OSError: [Errno 45] Operation not supported"
1582+
self.skipTest(f"chflags() doesn't support flags "
1583+
f"{flags:#b}: {exc}")
1584+
else:
1585+
os.chflags(filename, 0)
1586+
finally:
1587+
support.unlink(filename)
1588+
1589+
@unittest.skipUnless(hasattr(os, 'chflags'), 'requires os.lchflags')
1590+
def test_flags(self):
1591+
flags = stat.UF_IMMUTABLE | stat.UF_NOUNLINK
1592+
self.check_flags(flags)
1593+
1594+
d = self.do_create(recurse=3, dirs=2, files=2)
1595+
with d:
1596+
# Change files and directories flags recursively.
1597+
for root, dirs, files in os.walk(d.name, topdown=False):
1598+
for name in files:
1599+
os.chflags(os.path.join(root, name), flags)
1600+
os.chflags(root, flags)
1601+
d.cleanup()
1602+
self.assertFalse(os.path.exists(d.name))
1603+
14561604

14571605
if __name__ == "__main__":
14581606
unittest.main()

0 commit comments

Comments
 (0)