Skip to content

Commit 1f32c34

Browse files
authored
Merge pull request #1312 from douglasjacobsen/overwrite-stage-files
Update `stage_files` to overwrite named stages
2 parents 7e27514 + aec60b8 commit 1f32c34

File tree

4 files changed

+160
-53
lines changed

4 files changed

+160
-53
lines changed

lib/ramble/ramble/language/application_language.py

Lines changed: 54 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -431,9 +431,11 @@ def _define_cleanup(obj):
431431

432432
@application_directive("executables")
433433
def stage_files(
434-
src,
435-
dst,
434+
src=None,
435+
dst=None,
436+
stages=None,
436437
name=None,
438+
method="user-defined",
437439
when=None,
438440
**kwargs,
439441
):
@@ -447,21 +449,32 @@ def stage_files(
447449
option, which can be set to 'cp', 'rsync', 'symbolic_link', or 'hard_link'.
448450
449451
Args:
450-
src (str): The source path of the file or directory.
451-
dst (str): The destination path.
452+
src (str | None): The source path of the file or directory.
453+
dst (str | None): The destination path. If src is passed in, and dst is
454+
not, dst defaults to the experiment_run_dir.
455+
stages (list(tuple(str, str)) | None): A list of tuples describing pairs
456+
of src, dest locations to stage.
452457
name (str | None): The name of the executable. Defaults to 'stage-files'.
458+
method (str): The method to use for this stage. Can be one of:
459+
"user-defined", "cp", "rsync", "symbolic_link", "hard_link"
453460
when (list | None): List of when conditions to apply to this directive.
454461
"""
455462

463+
valid_methods = ["user-defined", "cp", "rsync", "symbolic_link", "hard_link"]
464+
465+
method_map = {
466+
"cp": "cp -Lr",
467+
"rsync": "rsync -Lr",
468+
"symbolic_link": "ln -sf",
469+
"hard_link": "ln -f",
470+
}
471+
456472
def _execute_stage_files(app):
457473
import os
458474

459475
import ramble.config
460476
from ramble.util.executable import CommandExecutable
461477

462-
cfg = ramble.config.config
463-
stage_method = cfg.get("config", {}).get("stage_method", "cp")
464-
465478
exec_name = name if name else "stage-files"
466479

467480
when_list = ramble.language.language_helpers.build_when_list(
@@ -481,28 +494,40 @@ def _execute_stage_files(app):
481494
"as an executable. Please provide a unique name attribute."
482495
)
483496

484-
# Prepare the core staging command
485-
if stage_method == "rsync":
486-
stage_cmd = f"rsync -Lr {src} {dst}"
487-
elif stage_method == "hard_link":
488-
stage_cmd = f"ln {src} {dst}"
489-
elif stage_method == "symbolic_link":
490-
stage_cmd = f"ln -s {src} {dst}"
491-
else: # stage_method == "cp"
492-
stage_cmd = f"cp -Lr {src} {dst}"
493-
494-
template = [stage_cmd]
495-
496-
# Prepend mkdir if dst has a parent directory
497-
parent_dir = os.path.dirname(dst)
498-
if parent_dir and parent_dir != ".":
499-
template.insert(0, f"mkdir -p {parent_dir}")
500-
501-
if exec_name in app.executables[when_set]:
502-
app.executables[when_set][exec_name].add_template(template)
503-
else:
504-
app.executables[when_set][exec_name] = CommandExecutable(
505-
name=exec_name, template=template, allow_extension=True, **kwargs
497+
if method not in valid_methods:
498+
raise DirectiveError(
499+
f"stage_files directive on application {app.name} was given an "
500+
f"invalid method argument of {method}.\n"
501+
f"Valid methods include: {valid_methods}"
506502
)
507503

504+
stage_method = method
505+
if stage_method == "user-defined":
506+
cfg = ramble.config.config
507+
stage_method = cfg.get("config", {}).get("stage_method", "cp")
508+
stage_cmd = method_map[stage_method]
509+
510+
template = []
511+
512+
if src is not None:
513+
if dst is not None:
514+
parent_dir = os.path.dirname(dst)
515+
if parent_dir and parent_dir != ".":
516+
template.insert(0, f"mkdir -p {parent_dir}")
517+
template.append(f"{stage_cmd} {src} {dst}")
518+
else:
519+
template.append(f"{stage_cmd} {src} {{experiment_run_dir}}/.")
520+
521+
if isinstance(stages, list):
522+
for pair_src, pair_dst in stages:
523+
parent_dir = os.path.dirname(pair_dst)
524+
if parent_dir and parent_dir != ".":
525+
template.append(f"mkdir -p {parent_dir}")
526+
527+
template.append(f"{stage_cmd} {pair_src} {pair_dst}")
528+
529+
app.executables[when_set][exec_name] = CommandExecutable(
530+
name=exec_name, template=template, allow_extension=True, **kwargs
531+
)
532+
508533
return _execute_stage_files

lib/ramble/ramble/test/application_language.py

Lines changed: 87 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -394,31 +394,106 @@ class BrokenWorkloadDefaults(ExecutableApplication): # noqa: F405
394394
assert "workload_defaults cannot be used with workload, workloads" in err
395395

396396

397+
def test_stage_files_directive_no_dst():
398+
import ramble.config
399+
400+
with ramble.config.override("config:stage_method", "cp"):
401+
402+
class TestApp(ExecutableApplication): # noqa: F405
403+
name = "test-app"
404+
405+
app_inst = TestApp("/not/a/path")
406+
app_inst.stage_files(src="src")
407+
408+
assert "stage-files" in app_inst.executables[frozenset()]
409+
exec = app_inst.executables[frozenset()]["stage-files"]
410+
assert exec.template == ["cp -Lr src {experiment_run_dir}/."]
411+
412+
413+
def test_stage_files_directive_stages():
414+
import ramble.config
415+
416+
with ramble.config.override("config:stage_method", "cp"):
417+
418+
class TestApp(ExecutableApplication): # noqa: F405
419+
name = "test-app"
420+
421+
app_inst = TestApp("/not/a/path")
422+
app_inst.stage_files(stages=[("src1", "a/b"), ("src2", "c/d")])
423+
424+
assert "stage-files" in app_inst.executables[frozenset()]
425+
exec = app_inst.executables[frozenset()]["stage-files"]
426+
assert exec.template == [
427+
"mkdir -p a",
428+
"cp -Lr src1 a/b",
429+
"mkdir -p c",
430+
"cp -Lr src2 c/d",
431+
]
432+
433+
434+
def test_stage_files_directive_overwrite():
435+
import ramble.config
436+
437+
with ramble.config.override("config:stage_method", "cp"):
438+
439+
class TestApp(ExecutableApplication): # noqa: F405
440+
name = "test-app"
441+
442+
app_inst = TestApp("/not/a/path")
443+
app_inst.stage_files(src="src", dst="a/b")
444+
445+
assert "stage-files" in app_inst.executables[frozenset()]
446+
exec = app_inst.executables[frozenset()]["stage-files"]
447+
assert exec.template == ["mkdir -p a", "cp -Lr src a/b"]
448+
449+
app_inst.stage_files(src="src2", dst="c/d")
450+
exec = app_inst.executables[frozenset()]["stage-files"]
451+
assert exec.template == ["mkdir -p c", "cp -Lr src2 c/d"]
452+
453+
397454
@pytest.mark.parametrize(
398455
"stage_method,template_contents",
399456
[
400-
("cp", "cp -Lr src"),
401-
("rsync", "rsync -Lr src"),
402-
("symbolic_link", "ln -s src"),
403-
("hard_link", "ln src"),
457+
("cp", "cp -Lr src dst"),
458+
("rsync", "rsync -Lr src dst"),
459+
("symbolic_link", "ln -sf src dst"),
460+
("hard_link", "ln -f src dst"),
404461
],
405462
)
406-
def test_stage_files_directive(stage_method, template_contents):
463+
def test_stage_files_directive_method(stage_method, template_contents):
464+
class TestApp(ExecutableApplication): # noqa: F405
465+
name = "test-app"
466+
467+
app_inst = TestApp("/not/a/path")
468+
app_inst.stage_files(src="src", dst="dst", method=stage_method)
469+
470+
assert "stage-files" in app_inst.executables[frozenset()]
471+
exec = app_inst.executables[frozenset()]["stage-files"]
472+
473+
assert exec.template == [template_contents]
474+
475+
476+
def test_stage_files_directive_user_defined():
407477
import ramble.config
408478

409-
with ramble.config.override("config:stage_method", stage_method):
479+
with ramble.config.override("config:stage_method", "rsync"):
410480

411481
class TestApp(ExecutableApplication): # noqa: F405
412482
name = "test-app"
413483

414484
app_inst = TestApp("/not/a/path")
415-
app_inst.stage_files(src="src", dst="dst")
485+
app_inst.stage_files(src="src", dst="dst", method="user-defined")
416486

417487
assert "stage-files" in app_inst.executables[frozenset()]
418488
exec = app_inst.executables[frozenset()]["stage-files"]
419489

420-
found = False
421-
for line in exec.template:
422-
if template_contents in line:
423-
found = True
424-
assert found
490+
assert exec.template == ["rsync -Lr src dst"]
491+
492+
493+
def test_stage_files_directive_invalid_method():
494+
class TestApp(ExecutableApplication): # noqa: F405
495+
name = "test-app"
496+
497+
app_inst = TestApp("/not/a/path")
498+
with pytest.raises(DirectiveError):
499+
app_inst.stage_files(src="src", dst="dst", method="invalid")

var/ramble/repos/builtin/applications/wrfv3/application.py

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -49,22 +49,23 @@ class Wrfv3(ExecutableApplication):
4949
)
5050

5151
stage_files(
52-
name="stage-run-files",
53-
src="{wrf_path}/run/*",
54-
dst="{experiment_run_dir}/.",
52+
stages=[
53+
("{wrf_path}/run/*", "{experiment_run_dir}/."),
54+
("{input_path}/*", "{experiment_run_dir}/."),
55+
]
5556
)
5657

5758
stage_files(
58-
name="stage-input",
59-
src="{input_path}/*",
59+
name="stage-namelist",
60+
method="cp",
61+
src="{input_path}/namelist.*",
6062
dst="{experiment_run_dir}/.",
6163
)
6264

6365
executable(
6466
"setup",
6567
template=[
6668
"rm -f rsl.* wrfout* namelist*",
67-
"cp {input_path}/namelist.* {experiment_run_dir}/.",
6869
],
6970
use_mpi=False,
7071
output_capture=OUTPUT_CAPTURE.ALL,
@@ -74,13 +75,13 @@ class Wrfv3(ExecutableApplication):
7475

7576
workload(
7677
"CONUS_2p5km",
77-
executables=["setup", "stage-input", "stage-run-files", "execute"],
78+
executables=["stage-files", "stage-namelist", "setup", "execute"],
7879
input="CONUS_2p5km",
7980
)
8081

8182
workload(
8283
"CONUS_12km",
83-
executables=["setup", "stage-input", "stage-run-files", "execute"],
84+
executables=["stage-files", "stage-namelist", "setup", "execute"],
8485
input="CONUS_12km",
8586
)
8687

var/ramble/repos/builtin/applications/wrfv4/application.py

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -88,20 +88,23 @@ class Wrfv4(ExecutableApplication):
8888
)
8989

9090
stage_files(
91-
src="{wrf_path}/run/*",
92-
dst="{experiment_run_dir}/.",
91+
stages=[
92+
("{wrf_path}/run/*", "{experiment_run_dir}/."),
93+
("{input_path}/*", "{experiment_run_dir}/."),
94+
]
9395
)
9496

9597
stage_files(
96-
src="{input_path}/*",
98+
name="stage-namelist",
99+
src="{input_path}/namelist.*",
97100
dst="{experiment_run_dir}/.",
101+
method="cp",
98102
)
99103

100104
executable(
101105
"setup",
102106
template=[
103107
"rm -f rsl.* wrfout* namelist*",
104-
"cp {input_path}/namelist.* {experiment_run_dir}/.",
105108
],
106109
use_mpi=False,
107110
output_capture=OUTPUT_CAPTURE.ALL,
@@ -156,6 +159,7 @@ class Wrfv4(ExecutableApplication):
156159
"CONUS_2p5km",
157160
executables=[
158161
"stage-files",
162+
"stage-namelist",
159163
"setup",
160164
"define_nproc_y",
161165
"define_nproc_x",
@@ -169,6 +173,7 @@ class Wrfv4(ExecutableApplication):
169173
"CONUS_12km",
170174
executables=[
171175
"stage-files",
176+
"stage-namelist",
172177
"setup",
173178
"define_nproc_y",
174179
"define_nproc_x",
@@ -183,6 +188,7 @@ class Wrfv4(ExecutableApplication):
183188
"Maria_1km",
184189
executables=[
185190
"stage-files",
191+
"stage-namelist",
186192
"setup",
187193
"define_nproc_y",
188194
"define_nproc_x",

0 commit comments

Comments
 (0)