Skip to content

Commit 4014d1d

Browse files
committed
Add function generating unused directories given a list of paths
The create_unused_dir function distinguishes between the parent directory and the name of the target subdirectory in 2 separate arguments. This interface requires additional parameter management when combined with newer path management interfaces such as pathlib. - Add a function create_unused_dirs accepting the target paths as a single argument. - Support the simultaneous creation of multiple unused paths with all of them having the same postfix if any of the actually requested paths is unavailable.
1 parent b217b38 commit 4014d1d

File tree

2 files changed

+223
-0
lines changed

2 files changed

+223
-0
lines changed

easybuild/tools/filetools.py

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2987,3 +2987,81 @@ def create_unused_dir(parent_folder, name):
29872987
set_gid_sticky_bits(path, recursive=True)
29882988

29892989
return path
2990+
2991+
2992+
def get_first_nonexisting_parent(path):
2993+
path = os.path.abspath(path)
2994+
2995+
first_nonexisting_parent = None
2996+
while not os.path.exists(path):
2997+
first_nonexisting_parent = path
2998+
path = os.path.dirname(path)
2999+
3000+
return first_nonexisting_parent
3001+
3002+
3003+
def _apply_postfix(paths, number):
3004+
postfix = '_' + str(number)
3005+
3006+
final_paths = []
3007+
first_nonexisting_parents = []
3008+
for p in paths:
3009+
final_paths.append(p + postfix)
3010+
first_nonexisting_parents.append(p)
3011+
3012+
return final_paths, first_nonexisting_parents
3013+
3014+
3015+
def _clean_directories(paths):
3016+
for p in paths:
3017+
shutil.rmtree(p, ignore_errors=True)
3018+
3019+
3020+
def create_unused_dirs(paths, index_upper_bound=10000):
3021+
"""
3022+
Create directories with given paths, including the parent directories
3023+
When a directory in the same path for any of the path in the path list already exists, then the suffix '_<i>' is
3024+
appended for i=0..(index_upper_bound-1) until an index is found so that all required path as unused. All created
3025+
directories have the same postfix.
3026+
3027+
:param paths: list of paths where directories are created; parent path created as well
3028+
:param index_upper_bound: maximum index that will be tried before failing
3029+
"""
3030+
paths = list(map(os.path.abspath, paths))
3031+
for path in paths:
3032+
for parent in set(paths) - {path}:
3033+
if is_predecesor(parent, path):
3034+
raise EasyBuildError("Path '%s' is predecesor of '%s'" % (parent, path))
3035+
3036+
final_paths = paths
3037+
first_nonexisting_parents = list(map(get_first_nonexisting_parent, paths))
3038+
paths_exist = False
3039+
number = -1
3040+
while number < index_upper_bound and not paths_exist: # Start with no suffix and limit the number of attempts
3041+
tried_paths = []
3042+
if number >= 0:
3043+
final_paths, first_nonexisting_parents = _apply_postfix(paths, number)
3044+
try:
3045+
for p in final_paths:
3046+
tried_paths.append(p)
3047+
os.makedirs(p)
3048+
paths_exist = True
3049+
except OSError as err:
3050+
# Distinguish between error due to existing folder and anything else
3051+
if tried_paths != [] and not os.path.exists(tried_paths[-1]):
3052+
_clean_directories(tried_paths[0:-1])
3053+
raise EasyBuildError("Failed to create directory %s: %s", tried_paths[-1], err)
3054+
_clean_directories(tried_paths[0:-1])
3055+
except BaseException as err:
3056+
_clean_directories(tried_paths)
3057+
raise err
3058+
number += 1
3059+
3060+
if number == index_upper_bound and not paths_exist:
3061+
raise EasyBuildError("Exceeded maximum number of attempts to generate unique directory: %s" % number)
3062+
3063+
# set group ID and sticky bits, if desired
3064+
for p in first_nonexisting_parents:
3065+
set_gid_sticky_bits(p, recursive=True)
3066+
3067+
return final_paths

test/framework/filetools.py

Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3544,6 +3544,31 @@ def test_compat_makedirs(self):
35443544
py2vs3.makedirs(name, exist_ok=True) # No error
35453545
self.assertExists(name)
35463546

3547+
def test_get_first_nonexisting_parent(self):
3548+
"""Test get_first_nonexisting_parent function."""
3549+
test_root = os.path.join(self.test_prefix, 'a')
3550+
3551+
base_path = os.path.join(self.test_prefix, 'a')
3552+
target_path = os.path.join(self.test_prefix, 'a', 'b', 'c')
3553+
os.makedirs(base_path)
3554+
first_nonexisting_parent = ft.get_first_nonexisting_parent(target_path)
3555+
self.assertEqual(first_nonexisting_parent, os.path.join(self.test_prefix, 'a', 'b'))
3556+
shutil.rmtree(test_root)
3557+
3558+
base_path = os.path.join(self.test_prefix, 'a', 'b')
3559+
target_path = os.path.join(self.test_prefix, 'a', 'b', 'c')
3560+
os.makedirs(base_path)
3561+
first_nonexisting_parent = ft.get_first_nonexisting_parent(target_path)
3562+
self.assertEqual(first_nonexisting_parent, os.path.join(self.test_prefix, 'a', 'b', 'c'))
3563+
shutil.rmtree(test_root)
3564+
3565+
base_path = os.path.join(self.test_prefix, 'a', 'b', 'c')
3566+
target_path = os.path.join(self.test_prefix, 'a', 'b', 'c')
3567+
os.makedirs(base_path)
3568+
first_nonexisting_parent = ft.get_first_nonexisting_parent(target_path)
3569+
self.assertEqual(first_nonexisting_parent, None)
3570+
shutil.rmtree(test_root)
3571+
35473572
def test_create_unused_dir(self):
35483573
"""Test create_unused_dir function."""
35493574
path = ft.create_unused_dir(self.test_prefix, 'folder')
@@ -3582,6 +3607,126 @@ def test_create_unused_dir(self):
35823607
self.assertEqual(path, os.path.join(self.test_prefix, 'file_0'))
35833608
self.assertExists(path)
35843609

3610+
def test_create_unused_dirs(self):
3611+
"""Test create_unused_dirs function."""
3612+
requested_paths = []
3613+
requested_paths.append(os.path.join(self.test_prefix, 'folder_a'))
3614+
requested_paths.append(os.path.join(self.test_prefix, 'folder_b'))
3615+
paths = ft.create_unused_dirs(requested_paths)
3616+
self.assertEqual(paths, requested_paths)
3617+
map(lambda p: self.assertExists(p), paths)
3618+
3619+
# Repeat with existing folder(s) should create new ones
3620+
for i in range(10):
3621+
requested_paths = []
3622+
requested_paths.append(os.path.join(self.test_prefix, 'folder_a'))
3623+
requested_paths.append(os.path.join(self.test_prefix, 'folder_b'))
3624+
paths = ft.create_unused_dirs(requested_paths)
3625+
self.assertEqual(paths, list(map(lambda p: p + '_%s' % i, requested_paths)))
3626+
map(lambda p: self.assertExists(p), paths)
3627+
3628+
# Skip suffix if a directory with the suffix already exists
3629+
existing_idx = 1
3630+
os.mkdir(os.path.join(self.test_prefix, "existing_idx_a"))
3631+
os.mkdir(os.path.join(self.test_prefix, "existing_idx_b"))
3632+
os.mkdir(os.path.join(self.test_prefix, f"existing_idx_b_{existing_idx}"))
3633+
3634+
def expected_idx(n):
3635+
if n >= existing_idx:
3636+
return n + 1
3637+
return n
3638+
3639+
for i in range(3):
3640+
requested_paths = []
3641+
requested_paths.append(os.path.join(self.test_prefix, 'existing_idx_a'))
3642+
requested_paths.append(os.path.join(self.test_prefix, 'existing_idx_b'))
3643+
paths = ft.create_unused_dirs(requested_paths)
3644+
self.assertEqual(paths, list(map(lambda p: p + '_%s' % expected_idx(i), requested_paths)))
3645+
map(lambda p: self.assertExists(p), paths)
3646+
self.assertNotExists(os.path.join(self.test_prefix, f"existing_idx_a_{existing_idx}"))
3647+
self.assertExists(os.path.join(self.test_prefix, f"existing_idx_b_{existing_idx}"))
3648+
3649+
# Support creation of parent directories
3650+
requested_paths = [os.path.join(self.test_prefix, 'parent_folder', 'folder')]
3651+
paths = ft.create_unused_dirs(requested_paths)
3652+
self.assertEqual(paths, requested_paths)
3653+
map(lambda p: self.assertExists(p), paths)
3654+
3655+
# Not influenced by similar folder
3656+
requested_paths = [os.path.join(self.test_prefix, 'folder_a2')]
3657+
paths = ft.create_unused_dirs(requested_paths)
3658+
self.assertEqual(paths, requested_paths)
3659+
map(lambda p: self.assertExists(p), paths)
3660+
for i in range(10):
3661+
paths = ft.create_unused_dirs(requested_paths)
3662+
self.assertEqual(paths, list(map(lambda p: p + '_%s' % i, requested_paths)))
3663+
map(lambda p: self.assertExists(p), paths)
3664+
3665+
# Fail cleanly if passed a readonly folder
3666+
readonly_dir = os.path.join(self.test_prefix, 'ro_folder')
3667+
ft.mkdir(readonly_dir)
3668+
old_perms = os.lstat(readonly_dir)[stat.ST_MODE]
3669+
ft.adjust_permissions(readonly_dir, stat.S_IREAD | stat.S_IEXEC, relative=False)
3670+
requested_path = os.path.join(readonly_dir, 'new_folder')
3671+
try:
3672+
self.assertErrorRegex(EasyBuildError, 'Failed to create directory',
3673+
ft.create_unused_dirs, [requested_path])
3674+
finally:
3675+
ft.adjust_permissions(readonly_dir, old_perms, relative=False)
3676+
3677+
# Fail if the number of attempts to create the directory is exceeded
3678+
os.mkdir(os.path.join(self.test_prefix, 'attempt'))
3679+
os.mkdir(os.path.join(self.test_prefix, 'attempt_0'))
3680+
os.mkdir(os.path.join(self.test_prefix, 'attempt_1'))
3681+
os.mkdir(os.path.join(self.test_prefix, 'attempt_2'))
3682+
os.mkdir(os.path.join(self.test_prefix, 'attempt_3'))
3683+
requested_path = os.path.join(self.test_prefix, 'attempt')
3684+
self.assertErrorRegex(
3685+
EasyBuildError,
3686+
'Exceeded maximum number of attempts to generate unique directory',
3687+
ft.create_unused_dirs,
3688+
[requested_path], index_upper_bound=4
3689+
)
3690+
3691+
# Ignore files same as folders. So first just create a file with no contents
3692+
requested_path = os.path.join(self.test_prefix, 'file')
3693+
ft.write_file(requested_path, '')
3694+
paths = ft.create_unused_dirs([requested_path])
3695+
self.assertEqual(paths, [requested_path + '_0'])
3696+
map(lambda p: self.assertExists(p), paths)
3697+
3698+
# Deny creation of nested directories
3699+
requested_paths = [
3700+
os.path.join(self.test_prefix, 'nested_a/foo/bar'),
3701+
os.path.join(self.test_prefix, 'nested_a/foo/bar/baz'),
3702+
]
3703+
self.assertErrorRegex(
3704+
EasyBuildError,
3705+
"Path '.*/foo/bar' is predecesor of '.*/foo/bar/baz'",
3706+
ft.create_unused_dirs,
3707+
requested_paths
3708+
)
3709+
requested_paths = [
3710+
os.path.join(self.test_prefix, 'nested_b/foo/bar/baz'),
3711+
os.path.join(self.test_prefix, 'nested_b/foo/bar'),
3712+
]
3713+
self.assertErrorRegex(
3714+
EasyBuildError,
3715+
"Path '.*/foo/bar' is predecesor of '.*/foo/bar/baz'",
3716+
ft.create_unused_dirs,
3717+
requested_paths
3718+
)
3719+
3720+
# Allow creation of non-nested directories
3721+
requested_paths = [
3722+
os.path.join(self.test_prefix, 'nested_c/foo/bar'),
3723+
os.path.join(self.test_prefix, 'nested_c/foo/baz'),
3724+
os.path.join(self.test_prefix, 'nested_c/buz'),
3725+
]
3726+
paths = ft.create_unused_dirs(requested_paths)
3727+
self.assertEqual(paths, requested_paths)
3728+
map(lambda p: self.assertExists(p), paths)
3729+
35853730

35863731
def suite():
35873732
""" returns all the testcases in this module """

0 commit comments

Comments
 (0)