hpcflow-new2 0.2.0a189__py3-none-any.whl → 0.2.0a199__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (176) hide show
  1. hpcflow/__pyinstaller/hook-hpcflow.py +9 -6
  2. hpcflow/_version.py +1 -1
  3. hpcflow/app.py +1 -0
  4. hpcflow/data/scripts/bad_script.py +2 -0
  5. hpcflow/data/scripts/do_nothing.py +2 -0
  6. hpcflow/data/scripts/env_specifier_test/input_file_generator_pass_env_spec.py +4 -0
  7. hpcflow/data/scripts/env_specifier_test/main_script_test_pass_env_spec.py +8 -0
  8. hpcflow/data/scripts/env_specifier_test/output_file_parser_pass_env_spec.py +4 -0
  9. hpcflow/data/scripts/env_specifier_test/v1/input_file_generator_basic.py +4 -0
  10. hpcflow/data/scripts/env_specifier_test/v1/main_script_test_direct_in_direct_out.py +7 -0
  11. hpcflow/data/scripts/env_specifier_test/v1/output_file_parser_basic.py +4 -0
  12. hpcflow/data/scripts/env_specifier_test/v2/main_script_test_direct_in_direct_out.py +7 -0
  13. hpcflow/data/scripts/input_file_generator_basic.py +3 -0
  14. hpcflow/data/scripts/input_file_generator_basic_FAIL.py +3 -0
  15. hpcflow/data/scripts/input_file_generator_test_stdout_stderr.py +8 -0
  16. hpcflow/data/scripts/main_script_test_direct_in.py +3 -0
  17. hpcflow/data/scripts/main_script_test_direct_in_direct_out_2.py +6 -0
  18. hpcflow/data/scripts/main_script_test_direct_in_direct_out_2_fail_allowed.py +6 -0
  19. hpcflow/data/scripts/main_script_test_direct_in_direct_out_2_fail_allowed_group.py +7 -0
  20. hpcflow/data/scripts/main_script_test_direct_in_direct_out_3.py +6 -0
  21. hpcflow/data/scripts/main_script_test_direct_in_group_direct_out_3.py +6 -0
  22. hpcflow/data/scripts/main_script_test_direct_in_group_one_fail_direct_out_3.py +6 -0
  23. hpcflow/data/scripts/main_script_test_hdf5_in_obj.py +1 -1
  24. hpcflow/data/scripts/main_script_test_hdf5_in_obj_2.py +12 -0
  25. hpcflow/data/scripts/main_script_test_hdf5_out_obj.py +1 -1
  26. hpcflow/data/scripts/main_script_test_json_out_FAIL.py +3 -0
  27. hpcflow/data/scripts/main_script_test_shell_env_vars.py +12 -0
  28. hpcflow/data/scripts/main_script_test_std_out_std_err.py +6 -0
  29. hpcflow/data/scripts/output_file_parser_basic.py +3 -0
  30. hpcflow/data/scripts/output_file_parser_basic_FAIL.py +7 -0
  31. hpcflow/data/scripts/output_file_parser_test_stdout_stderr.py +8 -0
  32. hpcflow/data/scripts/script_exit_test.py +5 -0
  33. hpcflow/data/template_components/environments.yaml +1 -1
  34. hpcflow/sdk/__init__.py +26 -15
  35. hpcflow/sdk/app.py +2192 -768
  36. hpcflow/sdk/cli.py +506 -296
  37. hpcflow/sdk/cli_common.py +105 -7
  38. hpcflow/sdk/config/__init__.py +1 -1
  39. hpcflow/sdk/config/callbacks.py +115 -43
  40. hpcflow/sdk/config/cli.py +126 -103
  41. hpcflow/sdk/config/config.py +674 -318
  42. hpcflow/sdk/config/config_file.py +131 -95
  43. hpcflow/sdk/config/errors.py +125 -84
  44. hpcflow/sdk/config/types.py +148 -0
  45. hpcflow/sdk/core/__init__.py +25 -1
  46. hpcflow/sdk/core/actions.py +1771 -1059
  47. hpcflow/sdk/core/app_aware.py +24 -0
  48. hpcflow/sdk/core/cache.py +139 -79
  49. hpcflow/sdk/core/command_files.py +263 -287
  50. hpcflow/sdk/core/commands.py +145 -112
  51. hpcflow/sdk/core/element.py +828 -535
  52. hpcflow/sdk/core/enums.py +192 -0
  53. hpcflow/sdk/core/environment.py +74 -93
  54. hpcflow/sdk/core/errors.py +455 -52
  55. hpcflow/sdk/core/execute.py +207 -0
  56. hpcflow/sdk/core/json_like.py +540 -272
  57. hpcflow/sdk/core/loop.py +751 -347
  58. hpcflow/sdk/core/loop_cache.py +164 -47
  59. hpcflow/sdk/core/object_list.py +370 -207
  60. hpcflow/sdk/core/parameters.py +1100 -627
  61. hpcflow/sdk/core/rule.py +59 -41
  62. hpcflow/sdk/core/run_dir_files.py +21 -37
  63. hpcflow/sdk/core/skip_reason.py +7 -0
  64. hpcflow/sdk/core/task.py +1649 -1339
  65. hpcflow/sdk/core/task_schema.py +308 -196
  66. hpcflow/sdk/core/test_utils.py +191 -114
  67. hpcflow/sdk/core/types.py +440 -0
  68. hpcflow/sdk/core/utils.py +485 -309
  69. hpcflow/sdk/core/validation.py +82 -9
  70. hpcflow/sdk/core/workflow.py +2544 -1178
  71. hpcflow/sdk/core/zarr_io.py +98 -137
  72. hpcflow/sdk/data/workflow_spec_schema.yaml +2 -0
  73. hpcflow/sdk/demo/cli.py +53 -33
  74. hpcflow/sdk/helper/cli.py +18 -15
  75. hpcflow/sdk/helper/helper.py +75 -63
  76. hpcflow/sdk/helper/watcher.py +61 -28
  77. hpcflow/sdk/log.py +122 -71
  78. hpcflow/sdk/persistence/__init__.py +8 -31
  79. hpcflow/sdk/persistence/base.py +1360 -606
  80. hpcflow/sdk/persistence/defaults.py +6 -0
  81. hpcflow/sdk/persistence/discovery.py +38 -0
  82. hpcflow/sdk/persistence/json.py +568 -188
  83. hpcflow/sdk/persistence/pending.py +382 -179
  84. hpcflow/sdk/persistence/store_resource.py +39 -23
  85. hpcflow/sdk/persistence/types.py +318 -0
  86. hpcflow/sdk/persistence/utils.py +14 -11
  87. hpcflow/sdk/persistence/zarr.py +1337 -433
  88. hpcflow/sdk/runtime.py +44 -41
  89. hpcflow/sdk/submission/{jobscript_info.py → enums.py} +39 -12
  90. hpcflow/sdk/submission/jobscript.py +1651 -692
  91. hpcflow/sdk/submission/schedulers/__init__.py +167 -39
  92. hpcflow/sdk/submission/schedulers/direct.py +121 -81
  93. hpcflow/sdk/submission/schedulers/sge.py +170 -129
  94. hpcflow/sdk/submission/schedulers/slurm.py +291 -268
  95. hpcflow/sdk/submission/schedulers/utils.py +12 -2
  96. hpcflow/sdk/submission/shells/__init__.py +14 -15
  97. hpcflow/sdk/submission/shells/base.py +150 -29
  98. hpcflow/sdk/submission/shells/bash.py +283 -173
  99. hpcflow/sdk/submission/shells/os_version.py +31 -30
  100. hpcflow/sdk/submission/shells/powershell.py +228 -170
  101. hpcflow/sdk/submission/submission.py +1014 -335
  102. hpcflow/sdk/submission/types.py +140 -0
  103. hpcflow/sdk/typing.py +182 -12
  104. hpcflow/sdk/utils/arrays.py +71 -0
  105. hpcflow/sdk/utils/deferred_file.py +55 -0
  106. hpcflow/sdk/utils/hashing.py +16 -0
  107. hpcflow/sdk/utils/patches.py +12 -0
  108. hpcflow/sdk/utils/strings.py +33 -0
  109. hpcflow/tests/api/test_api.py +32 -0
  110. hpcflow/tests/conftest.py +27 -6
  111. hpcflow/tests/data/multi_path_sequences.yaml +29 -0
  112. hpcflow/tests/data/workflow_test_run_abort.yaml +34 -35
  113. hpcflow/tests/schedulers/sge/test_sge_submission.py +36 -0
  114. hpcflow/tests/schedulers/slurm/test_slurm_submission.py +5 -2
  115. hpcflow/tests/scripts/test_input_file_generators.py +282 -0
  116. hpcflow/tests/scripts/test_main_scripts.py +866 -85
  117. hpcflow/tests/scripts/test_non_snippet_script.py +46 -0
  118. hpcflow/tests/scripts/test_ouput_file_parsers.py +353 -0
  119. hpcflow/tests/shells/wsl/test_wsl_submission.py +12 -4
  120. hpcflow/tests/unit/test_action.py +262 -75
  121. hpcflow/tests/unit/test_action_rule.py +9 -4
  122. hpcflow/tests/unit/test_app.py +33 -6
  123. hpcflow/tests/unit/test_cache.py +46 -0
  124. hpcflow/tests/unit/test_cli.py +134 -1
  125. hpcflow/tests/unit/test_command.py +71 -54
  126. hpcflow/tests/unit/test_config.py +142 -16
  127. hpcflow/tests/unit/test_config_file.py +21 -18
  128. hpcflow/tests/unit/test_element.py +58 -62
  129. hpcflow/tests/unit/test_element_iteration.py +50 -1
  130. hpcflow/tests/unit/test_element_set.py +29 -19
  131. hpcflow/tests/unit/test_group.py +4 -2
  132. hpcflow/tests/unit/test_input_source.py +116 -93
  133. hpcflow/tests/unit/test_input_value.py +29 -24
  134. hpcflow/tests/unit/test_jobscript_unit.py +757 -0
  135. hpcflow/tests/unit/test_json_like.py +44 -35
  136. hpcflow/tests/unit/test_loop.py +1396 -84
  137. hpcflow/tests/unit/test_meta_task.py +325 -0
  138. hpcflow/tests/unit/test_multi_path_sequences.py +229 -0
  139. hpcflow/tests/unit/test_object_list.py +17 -12
  140. hpcflow/tests/unit/test_parameter.py +29 -7
  141. hpcflow/tests/unit/test_persistence.py +237 -42
  142. hpcflow/tests/unit/test_resources.py +20 -18
  143. hpcflow/tests/unit/test_run.py +117 -6
  144. hpcflow/tests/unit/test_run_directories.py +29 -0
  145. hpcflow/tests/unit/test_runtime.py +2 -1
  146. hpcflow/tests/unit/test_schema_input.py +23 -15
  147. hpcflow/tests/unit/test_shell.py +23 -2
  148. hpcflow/tests/unit/test_slurm.py +8 -7
  149. hpcflow/tests/unit/test_submission.py +38 -89
  150. hpcflow/tests/unit/test_task.py +352 -247
  151. hpcflow/tests/unit/test_task_schema.py +33 -20
  152. hpcflow/tests/unit/test_utils.py +9 -11
  153. hpcflow/tests/unit/test_value_sequence.py +15 -12
  154. hpcflow/tests/unit/test_workflow.py +114 -83
  155. hpcflow/tests/unit/test_workflow_template.py +0 -1
  156. hpcflow/tests/unit/utils/test_arrays.py +40 -0
  157. hpcflow/tests/unit/utils/test_deferred_file_writer.py +34 -0
  158. hpcflow/tests/unit/utils/test_hashing.py +65 -0
  159. hpcflow/tests/unit/utils/test_patches.py +5 -0
  160. hpcflow/tests/unit/utils/test_redirect_std.py +50 -0
  161. hpcflow/tests/workflows/__init__.py +0 -0
  162. hpcflow/tests/workflows/test_directory_structure.py +31 -0
  163. hpcflow/tests/workflows/test_jobscript.py +334 -1
  164. hpcflow/tests/workflows/test_run_status.py +198 -0
  165. hpcflow/tests/workflows/test_skip_downstream.py +696 -0
  166. hpcflow/tests/workflows/test_submission.py +140 -0
  167. hpcflow/tests/workflows/test_workflows.py +160 -15
  168. hpcflow/tests/workflows/test_zip.py +18 -0
  169. hpcflow/viz_demo.ipynb +6587 -3
  170. {hpcflow_new2-0.2.0a189.dist-info → hpcflow_new2-0.2.0a199.dist-info}/METADATA +8 -4
  171. hpcflow_new2-0.2.0a199.dist-info/RECORD +221 -0
  172. hpcflow/sdk/core/parallel.py +0 -21
  173. hpcflow_new2-0.2.0a189.dist-info/RECORD +0 -158
  174. {hpcflow_new2-0.2.0a189.dist-info → hpcflow_new2-0.2.0a199.dist-info}/LICENSE +0 -0
  175. {hpcflow_new2-0.2.0a189.dist-info → hpcflow_new2-0.2.0a199.dist-info}/WHEEL +0 -0
  176. {hpcflow_new2-0.2.0a189.dist-info → hpcflow_new2-0.2.0a199.dist-info}/entry_points.txt +0 -0
hpcflow/sdk/cli_common.py CHANGED
@@ -1,17 +1,59 @@
1
1
  """Click CLI options that are used as decorators in multiple modules."""
2
2
 
3
+ from __future__ import annotations
3
4
  import click
4
5
 
5
6
  from hpcflow.sdk.core import ALL_TEMPLATE_FORMATS
6
- from hpcflow.sdk.persistence import ALL_STORE_FORMATS, DEFAULT_STORE_FORMAT
7
+ from hpcflow.sdk.persistence.defaults import DEFAULT_STORE_FORMAT
8
+ from hpcflow.sdk.persistence.discovery import ALL_STORE_FORMATS
7
9
 
8
10
 
9
- def sub_tasks_callback(ctx, param, value):
11
+ class BoolOrString(click.ParamType):
12
+ """Custom Click parameter type to accepts a bool or a choice of strings."""
13
+
14
+ name = "bool-or-string"
15
+
16
+ def __init__(self, allowed_strings, true_strings=None, false_strings=None):
17
+ self.allowed_strings = allowed_strings
18
+ self.true_strings = true_strings if true_strings else ["true", "yes", "on"]
19
+ self.false_strings = false_strings if false_strings else ["false", "no", "off"]
20
+
21
+ def convert(self, value, param, ctx):
22
+ # Check if the value is a boolean
23
+ if isinstance(value, bool):
24
+ return value
25
+
26
+ # Normalize value to string
27
+ value = str(value).lower()
28
+
29
+ # Check if the value is one of the true strings
30
+ if value in self.true_strings:
31
+ return True
32
+
33
+ # Check if the value is one of the false strings
34
+ if value in self.false_strings:
35
+ return False
36
+
37
+ # If the value matches neither, it must be one of the expected strings
38
+ if value not in self.allowed_strings:
39
+ allowed_fmt = ", ".join(f"{i!r}" for i in self.allowed_strings)
40
+ self.fail(
41
+ message=f"{value} is not a valid boolean or one of {allowed_fmt}.",
42
+ param=param,
43
+ ctx=ctx,
44
+ )
45
+
46
+ return value
47
+
48
+
49
+ def sub_tasks_callback(ctx, param, value: str | None) -> list[int] | None:
10
50
  """
11
51
  Parse subtasks.
12
52
  """
13
53
  if value:
14
54
  return [int(i) for i in value.split(",")]
55
+ else:
56
+ return None
15
57
 
16
58
 
17
59
  #: Standard option
@@ -91,12 +133,14 @@ variables_option = click.option(
91
133
  js_parallelism_option = click.option(
92
134
  "--js-parallelism",
93
135
  help=(
94
- "If True, allow multiple jobscripts to execute simultaneously. Raises if "
95
- "set to True but the store type does not support the "
96
- "`jobscript_parallelism` feature. If not set, jobscript parallelism will "
97
- "be used if the store type supports it."
136
+ "If True, allow multiple jobscripts to execute simultaneously. If "
137
+ "'scheduled'/'direct', only allow simultaneous execution of scheduled/direct "
138
+ "jobscripts. Raises if set to True, 'scheduled', or 'direct', but the store type "
139
+ "does not support the `jobscript_parallelism` feature. If not set, jobscript "
140
+ "parallelism will be used if the store type supports it, for scheduled "
141
+ "jobscripts only."
98
142
  ),
99
- type=click.BOOL,
143
+ type=BoolOrString(["direct", "scheduled"]),
100
144
  )
101
145
  #: Standard option
102
146
  wait_option = click.option(
@@ -140,6 +184,17 @@ submit_status_opt = click.option(
140
184
  help="If True, display a live status to track submission progress.",
141
185
  default=True,
142
186
  )
187
+ #: Standard option
188
+ force_arr_opt = click.option(
189
+ "--force-array",
190
+ help=(
191
+ "Used to force the use of job arrays, even if the scheduler does not support it. "
192
+ "This is provided for testing purposes only."
193
+ ),
194
+ is_flag=True,
195
+ default=False,
196
+ )
197
+
143
198
  #: Standard option
144
199
  make_status_opt = click.option(
145
200
  "--status/--no-status",
@@ -147,6 +202,14 @@ make_status_opt = click.option(
147
202
  default=True,
148
203
  )
149
204
 
205
+ #: Standard option
206
+ add_sub_opt = click.option(
207
+ "--add-submission",
208
+ help=("If True, add a submission to the workflow (but do not submit)."),
209
+ is_flag=True,
210
+ default=False,
211
+ )
212
+
150
213
  #: Standard option
151
214
  zip_path_opt = click.option(
152
215
  "--path",
@@ -207,6 +270,34 @@ rechunk_status_opt = click.option(
207
270
  default=True,
208
271
  help="If True, display a live status to track rechunking progress.",
209
272
  )
273
+ cancel_status_opt = click.option(
274
+ "--status/--no-status",
275
+ default=True,
276
+ help="If True, display a live status to track cancel progress.",
277
+ )
278
+
279
+ list_js_max_js_opt = click.option(
280
+ "--max-js", type=click.INT, help="Display up to this jobscript only."
281
+ )
282
+ list_js_jobscripts_opt = click.option(
283
+ "--jobscripts", help="Comma-separated list of jobscript indices to show."
284
+ )
285
+ list_task_js_max_js_opt = click.option(
286
+ "--max-js", type=click.INT, help="Include jobscripts up to this jobscript only."
287
+ )
288
+ list_task_js_task_names_opt = click.option(
289
+ "--task-names", help="Comma-separated list of task name sub-strings to show."
290
+ )
291
+ list_js_width_opt = click.option(
292
+ "--width", type=click.INT, help="Width in characters of the table to print."
293
+ )
294
+ jobscript_std_array_idx_opt = click.option(
295
+ "--array-idx",
296
+ type=click.INT,
297
+ help=(
298
+ "For array jobs only, the job array index whose standard stream is to be printed."
299
+ ),
300
+ )
210
301
 
211
302
 
212
303
  def _add_doc_from_help(*args):
@@ -255,4 +346,11 @@ _add_doc_from_help(
255
346
  rechunk_backup_opt,
256
347
  rechunk_chunk_size_opt,
257
348
  rechunk_status_opt,
349
+ cancel_status_opt,
350
+ list_js_max_js_opt,
351
+ list_js_jobscripts_opt,
352
+ list_task_js_max_js_opt,
353
+ list_task_js_task_names_opt,
354
+ list_js_width_opt,
355
+ jobscript_std_array_idx_opt,
258
356
  )
@@ -2,4 +2,4 @@
2
2
  Configuration loading and manipulation.
3
3
  """
4
4
 
5
- from .config import Config, ConfigFile, ConfigOptions, DEFAULT_CONFIG
5
+ from hpcflow.sdk.config.config import Config, ConfigFile, ConfigOptions, DEFAULT_CONFIG
@@ -1,44 +1,58 @@
1
1
  """Module that defines built-in callback functions for configuration item values."""
2
2
 
3
-
3
+ from __future__ import annotations
4
4
  import os
5
5
  import re
6
- import fsspec
6
+ import fsspec # type: ignore
7
+ from typing import overload, TYPE_CHECKING
7
8
  from hpcflow.sdk.core.errors import UnsupportedSchedulerError, UnsupportedShellError
8
-
9
9
  from hpcflow.sdk.submission.shells import get_supported_shells
10
10
 
11
+ if TYPE_CHECKING:
12
+ from typing import Any, TypeVar
13
+ from .config import Config
14
+ from ..typing import PathLike
15
+
16
+ T = TypeVar("T")
11
17
 
12
- def callback_vars(config, value):
18
+
19
+ def callback_vars(config: Config, value) -> str:
13
20
  """
14
21
  Callback that substitutes configuration variables.
15
22
  """
16
23
 
17
- def vars_repl(match_obj):
18
- var_name = match_obj.groups()[0]
19
- return config._variables[var_name]
24
+ def vars_repl(match_obj: re.Match[str]) -> str:
25
+ return config._variables[match_obj[1]]
20
26
 
21
- vars_join = "|".join(list(config._variables.keys()))
22
- vars_regex = r"\<\<(" + vars_join + r")\>\>"
23
- value = re.sub(
27
+ vars_regex = rf"\<\<({ '|'.join(config._variables) })\>\>"
28
+ return re.sub(
24
29
  pattern=vars_regex,
25
30
  repl=vars_repl,
26
31
  string=str(value),
27
32
  )
28
- return value
29
33
 
30
34
 
31
- def callback_file_paths(config, file_path):
35
+ @overload
36
+ def callback_file_paths(config: Config, file_path: PathLike) -> PathLike:
37
+ ...
38
+
39
+
40
+ @overload
41
+ def callback_file_paths(config: Config, file_path: list[PathLike]) -> list[PathLike]:
42
+ ...
43
+
44
+
45
+ def callback_file_paths(config: Config, file_path: PathLike | list[PathLike]):
32
46
  """
33
47
  Callback that resolves file paths.
34
48
  """
35
49
  if isinstance(file_path, list):
36
- return [config._resolve_path(i) for i in file_path]
50
+ return [config._resolve_path(path) for path in file_path]
37
51
  else:
38
52
  return config._resolve_path(file_path)
39
53
 
40
54
 
41
- def callback_bool(config, value):
55
+ def callback_bool(config: Config, value: str | bool) -> bool:
42
56
  """
43
57
  Callback that coerces values to boolean.
44
58
  """
@@ -52,19 +66,36 @@ def callback_bool(config, value):
52
66
  return value
53
67
 
54
68
 
55
- def callback_lowercase(config, value):
69
+ @overload
70
+ def callback_lowercase(config: Config, value: list[str]) -> list[str]:
71
+ ...
72
+
73
+
74
+ @overload
75
+ def callback_lowercase(config: Config, value: dict[str, T]) -> dict[str, T]:
76
+ ...
77
+
78
+
79
+ @overload
80
+ def callback_lowercase(config: Config, value: str) -> str:
81
+ ...
82
+
83
+
84
+ def callback_lowercase(
85
+ config: Config, value: list[str] | dict[str, T] | str
86
+ ) -> list[str] | dict[str, T] | str:
56
87
  """
57
88
  Callback that forces a string to lower case.
58
89
  """
59
90
  if isinstance(value, list):
60
- return [i.lower() for i in value]
91
+ return [item.lower() for item in value]
61
92
  elif isinstance(value, dict):
62
93
  return {k.lower(): v for k, v in value.items()}
63
94
  else:
64
95
  return value.lower()
65
96
 
66
97
 
67
- def exists_in_schedulers(config, value):
98
+ def exists_in_schedulers(config: Config, value: T) -> T:
68
99
  """
69
100
  Callback that tests that a value is a supported scheduler name.
70
101
  """
@@ -77,21 +108,25 @@ def exists_in_schedulers(config, value):
77
108
  return value
78
109
 
79
110
 
80
- def callback_supported_schedulers(config, schedulers):
111
+ def callback_supported_schedulers(
112
+ config: Config, schedulers: dict[str, Any]
113
+ ) -> dict[str, Any]:
81
114
  """
82
115
  Callback that tests that all values are names of supported schedulers.
83
116
  """
84
117
  # validate against supported schedulers according to the OS - this won't validate that
85
118
  # a particular scheduler actually exists on this system:
86
- available = config._app.get_OS_supported_schedulers()
87
- for k in schedulers:
88
- if k not in available:
89
- raise UnsupportedSchedulerError(scheduler=k, available=available)
90
-
119
+ available = set(config._app.get_OS_supported_schedulers())
120
+ if any((witness := k) not in available for k in schedulers):
121
+ raise UnsupportedSchedulerError(scheduler=witness, available=available)
91
122
  return schedulers
92
123
 
93
124
 
94
- def set_scheduler_invocation_match(config, scheduler: str):
125
+ def _hostname_in_invocation(config: Config) -> bool:
126
+ return "hostname" in config._file.get_invocation(config._config_key)["match"]
127
+
128
+
129
+ def set_scheduler_invocation_match(config: Config, scheduler: str) -> None:
95
130
  """Invoked on set of `default_scheduler`.
96
131
 
97
132
  For clusters with "proper" schedulers (SGE, SLURM, etc.), login nodes are typically
@@ -100,25 +135,25 @@ def set_scheduler_invocation_match(config, scheduler: str):
100
135
  that on clusters the hostname match is explicitly set.
101
136
 
102
137
  """
103
- default_args = config.get(f"schedulers.{scheduler}").get("defaults", {})
104
138
  sched = config._app.get_scheduler(
105
139
  scheduler_name=scheduler,
106
140
  os_name=os.name,
107
- scheduler_args=default_args,
141
+ scheduler_args=config.get(f"schedulers.{scheduler}").get("defaults", {}),
108
142
  )
109
- if hasattr(sched, "DEFAULT_LOGIN_NODE_MATCH"):
110
- if "hostname" not in config._file.get_invocation(config._config_key)["match"]:
143
+ if isinstance(sched, config._app.QueuedScheduler):
144
+ if not _hostname_in_invocation(config):
111
145
  config._file.update_invocation(
112
146
  config_key=config._config_key,
113
147
  match={"hostname": sched.DEFAULT_LOGIN_NODE_MATCH},
114
148
  )
115
149
 
116
150
 
117
- def callback_scheduler_set_up(config, schedulers):
151
+ def callback_scheduler_set_up(
152
+ config: Config, schedulers: dict[str, Any]
153
+ ) -> dict[str, Any]:
118
154
  """Invoked on set of `schedulers`.
119
155
 
120
156
  Runs scheduler-specific config initialisation.
121
-
122
157
  """
123
158
  for k, v in schedulers.items():
124
159
  sched = config._app.get_scheduler(
@@ -126,19 +161,19 @@ def callback_scheduler_set_up(config, schedulers):
126
161
  os_name=os.name,
127
162
  scheduler_args=v.get("defaults", {}),
128
163
  )
129
- if hasattr(sched, "get_login_nodes"):
130
- # some `Scheduler` classes have a `get_login_nodes` method which can be used
164
+
165
+ if isinstance(sched, config._app.SGEPosix):
166
+ # some `QueuedScheduler` classes have a `get_login_nodes` method which can be used
131
167
  # to populate the names of login nodes explicitly, if not already set:
132
- if "hostname" not in config._file.get_invocation(config._config_key)["match"]:
133
- login_nodes = sched.get_login_nodes()
168
+ if not _hostname_in_invocation(config):
134
169
  config._file.update_invocation(
135
170
  config_key=config._config_key,
136
- match={"hostname": login_nodes},
171
+ match={"hostname": sched.get_login_nodes()},
137
172
  )
138
173
  return schedulers
139
174
 
140
175
 
141
- def callback_supported_shells(config, shell_name):
176
+ def callback_supported_shells(config: Config, shell_name: str) -> str:
142
177
  """
143
178
  Callback that tests if a shell names is supported on this OS.
144
179
  """
@@ -148,32 +183,69 @@ def callback_supported_shells(config, shell_name):
148
183
  return shell_name
149
184
 
150
185
 
151
- def set_callback_file_paths(config, value):
186
+ def set_callback_file_paths(config: Config, value: PathLike | list[PathLike]) -> None:
152
187
  """Check the file(s) is/are accessible. This is only done on `config.set` (and not on
153
188
  `config.get` or `config._validate`) because it could be expensive in the case of remote
154
189
  files."""
155
190
  value = callback_file_paths(config, value)
156
191
 
157
- to_check = value
158
- if not isinstance(value, list):
159
- to_check = [value]
192
+ to_check = value if isinstance(value, list) else [value]
160
193
 
161
194
  for file_path in to_check:
195
+ if file_path is None:
196
+ continue
162
197
  with fsspec.open(file_path, mode="rt") as fh:
163
198
  pass
164
199
  # TODO: also check something in it?
165
200
  print(f"Checked access to: {file_path}")
166
201
 
167
202
 
168
- def check_load_data_files(config, value):
203
+ def check_load_data_files(config: Config, value: Any) -> None:
169
204
  """Check data files (e.g., task schema files) can be loaded successfully. This is only
170
205
  done on `config.set` (and not on `config.get` or `config._validate`) because it could
171
206
  be expensive in the case of remote files."""
172
207
  config._app.reload_template_components(warn=False)
173
208
 
174
209
 
175
- def callback_update_log_console_level(config, value):
210
+ def callback_log_file_path(config, value):
211
+ value = value.strip()
212
+ if value:
213
+ return config._resolve_path(value)
214
+ else:
215
+ return value
216
+
217
+
218
+ def callback_update_log_console_level(config: Config, value: str) -> None:
176
219
  """
177
220
  Callback to set the logging level.
178
221
  """
179
- config._app.log.update_console_level(value)
222
+ config._app.log.update_console_level(new_level=value)
223
+
224
+
225
+ def callback_unset_log_console_level(config: Config) -> None:
226
+ """Reset the console handler to the default level."""
227
+ config._app.log.update_console_level()
228
+
229
+
230
+ def callback_update_log_file_level(config: Config, value: str) -> None:
231
+ """Callback to set the level of the log file handler."""
232
+ config._app.log.update_file_level(new_level=value)
233
+
234
+
235
+ def callback_update_log_file_path(config: Config, value: str) -> None:
236
+ """
237
+ Callback to update the log file path, or remove the file handler if no path specifed.
238
+ """
239
+ config._app.log.remove_file_handler()
240
+ if value:
241
+ config._app.log.add_file_logger(path=value, level=config.get("log_file_level"))
242
+
243
+
244
+ def callback_unset_log_file_level(config: Config) -> None:
245
+ """Callback to reset the file handler to the default level."""
246
+ config._app.log.update_file_level()
247
+
248
+
249
+ def callback_unset_log_file_path(config: Config) -> None:
250
+ """Callback to remove the log file handler."""
251
+ config._app.log.remove_file_handler()