hpcflow-new2 0.2.0a189__py3-none-any.whl → 0.2.0a190__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 (115) hide show
  1. hpcflow/__pyinstaller/hook-hpcflow.py +8 -6
  2. hpcflow/_version.py +1 -1
  3. hpcflow/app.py +1 -0
  4. hpcflow/data/scripts/main_script_test_hdf5_in_obj.py +1 -1
  5. hpcflow/data/scripts/main_script_test_hdf5_out_obj.py +1 -1
  6. hpcflow/sdk/__init__.py +21 -15
  7. hpcflow/sdk/app.py +2133 -770
  8. hpcflow/sdk/cli.py +281 -250
  9. hpcflow/sdk/cli_common.py +6 -2
  10. hpcflow/sdk/config/__init__.py +1 -1
  11. hpcflow/sdk/config/callbacks.py +77 -42
  12. hpcflow/sdk/config/cli.py +126 -103
  13. hpcflow/sdk/config/config.py +578 -311
  14. hpcflow/sdk/config/config_file.py +131 -95
  15. hpcflow/sdk/config/errors.py +112 -85
  16. hpcflow/sdk/config/types.py +145 -0
  17. hpcflow/sdk/core/actions.py +1054 -994
  18. hpcflow/sdk/core/app_aware.py +24 -0
  19. hpcflow/sdk/core/cache.py +81 -63
  20. hpcflow/sdk/core/command_files.py +275 -185
  21. hpcflow/sdk/core/commands.py +111 -107
  22. hpcflow/sdk/core/element.py +724 -503
  23. hpcflow/sdk/core/enums.py +192 -0
  24. hpcflow/sdk/core/environment.py +74 -93
  25. hpcflow/sdk/core/errors.py +398 -51
  26. hpcflow/sdk/core/json_like.py +540 -272
  27. hpcflow/sdk/core/loop.py +380 -334
  28. hpcflow/sdk/core/loop_cache.py +160 -43
  29. hpcflow/sdk/core/object_list.py +370 -207
  30. hpcflow/sdk/core/parameters.py +728 -600
  31. hpcflow/sdk/core/rule.py +59 -41
  32. hpcflow/sdk/core/run_dir_files.py +33 -22
  33. hpcflow/sdk/core/task.py +1546 -1325
  34. hpcflow/sdk/core/task_schema.py +240 -196
  35. hpcflow/sdk/core/test_utils.py +126 -88
  36. hpcflow/sdk/core/types.py +387 -0
  37. hpcflow/sdk/core/utils.py +410 -305
  38. hpcflow/sdk/core/validation.py +82 -9
  39. hpcflow/sdk/core/workflow.py +1192 -1028
  40. hpcflow/sdk/core/zarr_io.py +98 -137
  41. hpcflow/sdk/demo/cli.py +46 -33
  42. hpcflow/sdk/helper/cli.py +18 -16
  43. hpcflow/sdk/helper/helper.py +75 -63
  44. hpcflow/sdk/helper/watcher.py +61 -28
  45. hpcflow/sdk/log.py +83 -59
  46. hpcflow/sdk/persistence/__init__.py +8 -31
  47. hpcflow/sdk/persistence/base.py +988 -586
  48. hpcflow/sdk/persistence/defaults.py +6 -0
  49. hpcflow/sdk/persistence/discovery.py +38 -0
  50. hpcflow/sdk/persistence/json.py +408 -153
  51. hpcflow/sdk/persistence/pending.py +158 -123
  52. hpcflow/sdk/persistence/store_resource.py +37 -22
  53. hpcflow/sdk/persistence/types.py +307 -0
  54. hpcflow/sdk/persistence/utils.py +14 -11
  55. hpcflow/sdk/persistence/zarr.py +477 -420
  56. hpcflow/sdk/runtime.py +44 -41
  57. hpcflow/sdk/submission/{jobscript_info.py → enums.py} +39 -12
  58. hpcflow/sdk/submission/jobscript.py +444 -404
  59. hpcflow/sdk/submission/schedulers/__init__.py +133 -40
  60. hpcflow/sdk/submission/schedulers/direct.py +97 -71
  61. hpcflow/sdk/submission/schedulers/sge.py +132 -126
  62. hpcflow/sdk/submission/schedulers/slurm.py +263 -268
  63. hpcflow/sdk/submission/schedulers/utils.py +7 -2
  64. hpcflow/sdk/submission/shells/__init__.py +14 -15
  65. hpcflow/sdk/submission/shells/base.py +102 -29
  66. hpcflow/sdk/submission/shells/bash.py +72 -55
  67. hpcflow/sdk/submission/shells/os_version.py +31 -30
  68. hpcflow/sdk/submission/shells/powershell.py +37 -29
  69. hpcflow/sdk/submission/submission.py +203 -257
  70. hpcflow/sdk/submission/types.py +143 -0
  71. hpcflow/sdk/typing.py +163 -12
  72. hpcflow/tests/conftest.py +8 -6
  73. hpcflow/tests/schedulers/slurm/test_slurm_submission.py +5 -2
  74. hpcflow/tests/scripts/test_main_scripts.py +60 -30
  75. hpcflow/tests/shells/wsl/test_wsl_submission.py +6 -4
  76. hpcflow/tests/unit/test_action.py +86 -75
  77. hpcflow/tests/unit/test_action_rule.py +9 -4
  78. hpcflow/tests/unit/test_app.py +13 -6
  79. hpcflow/tests/unit/test_cli.py +1 -1
  80. hpcflow/tests/unit/test_command.py +71 -54
  81. hpcflow/tests/unit/test_config.py +20 -15
  82. hpcflow/tests/unit/test_config_file.py +21 -18
  83. hpcflow/tests/unit/test_element.py +58 -62
  84. hpcflow/tests/unit/test_element_iteration.py +3 -1
  85. hpcflow/tests/unit/test_element_set.py +29 -19
  86. hpcflow/tests/unit/test_group.py +4 -2
  87. hpcflow/tests/unit/test_input_source.py +116 -93
  88. hpcflow/tests/unit/test_input_value.py +29 -24
  89. hpcflow/tests/unit/test_json_like.py +44 -35
  90. hpcflow/tests/unit/test_loop.py +65 -58
  91. hpcflow/tests/unit/test_object_list.py +17 -12
  92. hpcflow/tests/unit/test_parameter.py +16 -7
  93. hpcflow/tests/unit/test_persistence.py +48 -35
  94. hpcflow/tests/unit/test_resources.py +20 -18
  95. hpcflow/tests/unit/test_run.py +8 -3
  96. hpcflow/tests/unit/test_runtime.py +2 -1
  97. hpcflow/tests/unit/test_schema_input.py +23 -15
  98. hpcflow/tests/unit/test_shell.py +3 -2
  99. hpcflow/tests/unit/test_slurm.py +8 -7
  100. hpcflow/tests/unit/test_submission.py +39 -19
  101. hpcflow/tests/unit/test_task.py +352 -247
  102. hpcflow/tests/unit/test_task_schema.py +33 -20
  103. hpcflow/tests/unit/test_utils.py +9 -11
  104. hpcflow/tests/unit/test_value_sequence.py +15 -12
  105. hpcflow/tests/unit/test_workflow.py +114 -83
  106. hpcflow/tests/unit/test_workflow_template.py +0 -1
  107. hpcflow/tests/workflows/test_jobscript.py +2 -1
  108. hpcflow/tests/workflows/test_workflows.py +18 -13
  109. {hpcflow_new2-0.2.0a189.dist-info → hpcflow_new2-0.2.0a190.dist-info}/METADATA +2 -1
  110. hpcflow_new2-0.2.0a190.dist-info/RECORD +165 -0
  111. hpcflow/sdk/core/parallel.py +0 -21
  112. hpcflow_new2-0.2.0a189.dist-info/RECORD +0 -158
  113. {hpcflow_new2-0.2.0a189.dist-info → hpcflow_new2-0.2.0a190.dist-info}/LICENSE +0 -0
  114. {hpcflow_new2-0.2.0a189.dist-info → hpcflow_new2-0.2.0a190.dist-info}/WHEEL +0 -0
  115. {hpcflow_new2-0.2.0a189.dist-info → hpcflow_new2-0.2.0a190.dist-info}/entry_points.txt +0 -0
hpcflow/sdk/cli.py CHANGED
@@ -2,12 +2,13 @@
2
2
  Command line interface implementation.
3
3
  """
4
4
 
5
+ from __future__ import annotations
5
6
  import json
6
7
  import os
7
- from typing import Dict, List
8
8
  import click
9
9
  from colorama import init as colorama_init
10
- from termcolor import colored
10
+ from termcolor import colored # type: ignore
11
+ from typing import TYPE_CHECKING
11
12
  from rich.pretty import pprint
12
13
 
13
14
  from hpcflow import __version__, _app_name
@@ -46,7 +47,16 @@ from hpcflow.sdk.cli_common import (
46
47
  )
47
48
  from hpcflow.sdk.helper.cli import get_helper_CLI
48
49
  from hpcflow.sdk.log import TimeIt
50
+ from hpcflow.sdk.core.workflow import Workflow
49
51
  from hpcflow.sdk.submission.shells import ALL_SHELLS
52
+ from hpcflow.sdk.submission.jobscript import Jobscript
53
+ from hpcflow.sdk.submission.submission import Submission
54
+ from hpcflow.sdk.submission.schedulers.sge import SGEPosix
55
+
56
+ if TYPE_CHECKING:
57
+ from pathlib import Path
58
+ from typing import Literal
59
+ from .app import BaseApp
50
60
 
51
61
  #: Standard option
52
62
  string_option = click.option(
@@ -59,16 +69,22 @@ string_option = click.option(
59
69
  workflow_ref_type_opt = click.option(
60
70
  "--ref-type",
61
71
  "-r",
62
- type=click.Choice(["assume-id", "id", "path"]),
72
+ type=click.Choice(("assume-id", "id", "path")),
63
73
  default="assume-id",
64
74
  help="How to interpret a reference, as an ID, a path, or to guess.",
65
75
  )
66
76
 
77
+ #: Get the current workflow from the context.
78
+ _pass_workflow = click.make_pass_decorator(Workflow)
79
+ #: Get the current submission from the context.
80
+ _pass_submission = click.make_pass_decorator(Submission)
81
+ #: Get the current jobscript from the context.
82
+ _pass_js = click.make_pass_decorator(Jobscript)
67
83
 
68
84
  _add_doc_from_help(string_option, workflow_ref_type_opt)
69
85
 
70
86
 
71
- def parse_jobscript_wait_spec(jobscripts: str) -> Dict[int, List[int]]:
87
+ def parse_jobscript_wait_spec(jobscripts: str) -> dict[int, list[int]]:
72
88
  """
73
89
  Parse a jobscript wait specification.
74
90
  """
@@ -79,7 +95,15 @@ def parse_jobscript_wait_spec(jobscripts: str) -> Dict[int, List[int]]:
79
95
  return sub_js_idx_dct
80
96
 
81
97
 
82
- def _make_API_CLI(app):
98
+ def _set_help_name(cmd: click.Group | click.Command, app: BaseApp):
99
+ """
100
+ Update the help string of the command to contain the name of the application.
101
+ """
102
+ if cmd.help:
103
+ cmd.help = cmd.help.format(app_name=app.name)
104
+
105
+
106
+ def _make_API_CLI(app: BaseApp):
83
107
  """Generate the CLI for the main functionality."""
84
108
 
85
109
  @click.command(name="make")
@@ -95,17 +119,17 @@ def _make_API_CLI(app):
95
119
  @variables_option
96
120
  @make_status_opt
97
121
  def make_workflow(
98
- template_file_or_str,
99
- string,
100
- format,
101
- path,
102
- name,
103
- overwrite,
104
- store,
105
- ts_fmt=None,
106
- ts_name_fmt=None,
107
- variables=None,
108
- status=True,
122
+ template_file_or_str: str,
123
+ string: bool,
124
+ format: Literal["json", "yaml"] | None,
125
+ path: Path | None,
126
+ name: str | None,
127
+ overwrite: bool,
128
+ store: str,
129
+ ts_fmt: str | None = None,
130
+ ts_name_fmt: str | None = None,
131
+ variables: list[tuple[str, str]] | None = None,
132
+ status: bool = True,
109
133
  ):
110
134
  """Generate a new {app_name} workflow.
111
135
 
@@ -123,7 +147,7 @@ def _make_API_CLI(app):
123
147
  store=store,
124
148
  ts_fmt=ts_fmt,
125
149
  ts_name_fmt=ts_name_fmt,
126
- variables=dict(variables),
150
+ variables=dict(variables) if variables is not None else None,
127
151
  status=status,
128
152
  )
129
153
  click.echo(wk.path)
@@ -147,23 +171,23 @@ def _make_API_CLI(app):
147
171
  @cancel_opt
148
172
  @submit_status_opt
149
173
  def make_and_submit_workflow(
150
- template_file_or_str,
151
- string,
152
- format,
153
- path,
154
- name,
155
- overwrite,
156
- store,
157
- ts_fmt=None,
158
- ts_name_fmt=None,
159
- variables=None,
160
- js_parallelism=None,
161
- wait=False,
162
- add_to_known=True,
163
- print_idx=False,
164
- tasks=None,
165
- cancel=False,
166
- status=True,
174
+ template_file_or_str: str,
175
+ string: bool,
176
+ format: Literal["json", "yaml"] | None,
177
+ path: Path | None,
178
+ name: str | None,
179
+ overwrite: bool,
180
+ store: str,
181
+ ts_fmt: str | None = None,
182
+ ts_name_fmt: str | None = None,
183
+ variables: list[tuple[str, str]] | None = None,
184
+ js_parallelism: bool | None = None,
185
+ wait: bool = False,
186
+ add_to_known: bool = True,
187
+ print_idx: bool = False,
188
+ tasks: list[int] | None = None,
189
+ cancel: bool = False,
190
+ status: bool = True,
167
191
  ):
168
192
  """Generate and submit a new {app_name} workflow.
169
193
 
@@ -182,7 +206,7 @@ def _make_API_CLI(app):
182
206
  store=store,
183
207
  ts_fmt=ts_fmt,
184
208
  ts_name_fmt=ts_name_fmt,
185
- variables=dict(variables),
209
+ variables=dict(variables) if variables is not None else None,
186
210
  JS_parallelism=js_parallelism,
187
211
  wait=wait,
188
212
  add_to_known=add_to_known,
@@ -192,12 +216,13 @@ def _make_API_CLI(app):
192
216
  status=status,
193
217
  )
194
218
  if print_idx:
219
+ assert isinstance(out, tuple)
195
220
  click.echo(out[1])
196
221
 
197
222
  @click.command(context_settings={"ignore_unknown_options": True})
198
223
  @click.argument("py_test_args", nargs=-1, type=click.UNPROCESSED)
199
224
  @click.pass_context
200
- def test(ctx, py_test_args):
225
+ def test(ctx: click.Context, py_test_args: list[str]):
201
226
  """Run {app_name} test suite.
202
227
 
203
228
  PY_TEST_ARGS are arguments passed on to Pytest.
@@ -208,7 +233,7 @@ def _make_API_CLI(app):
208
233
  @click.command(context_settings={"ignore_unknown_options": True})
209
234
  @click.argument("py_test_args", nargs=-1, type=click.UNPROCESSED)
210
235
  @click.pass_context
211
- def test_hpcflow(ctx, py_test_args):
236
+ def test_hpcflow(ctx: click.Context, py_test_args: list[str]):
212
237
  """Run hpcFlow test suite.
213
238
 
214
239
  PY_TEST_ARGS are arguments passed on to Pytest.
@@ -222,8 +247,7 @@ def _make_API_CLI(app):
222
247
  test,
223
248
  ]
224
249
  for cmd in commands:
225
- if cmd.help:
226
- cmd.help = cmd.help.format(app_name=app.name)
250
+ _set_help_name(cmd, app)
227
251
 
228
252
  if app.name != "hpcFlow":
229
253
  # `test_hpcflow` is the same as `test` for the hpcflow app no need to add both:
@@ -232,118 +256,116 @@ def _make_API_CLI(app):
232
256
  return commands
233
257
 
234
258
 
235
- def _make_workflow_submission_jobscript_CLI(app):
259
+ def _make_workflow_submission_jobscript_CLI(app: BaseApp):
236
260
  """Generate the CLI for interacting with existing workflow submission
237
261
  jobscripts."""
238
262
 
239
263
  @click.group(name="js")
264
+ @_pass_submission
240
265
  @click.pass_context
241
266
  @click.argument("js_idx", type=click.INT)
242
- def jobscript(ctx, js_idx):
267
+ def jobscript(ctx: click.Context, sb: Submission, js_idx: int):
243
268
  """Interact with existing {app_name} workflow submission jobscripts.
244
269
 
245
270
  JS_IDX is the jobscript index within the submission object.
246
271
 
247
272
  """
248
- ctx.obj["jobscript"] = ctx.obj["submission"].jobscripts[js_idx]
273
+ ctx.obj = sb.jobscripts[js_idx]
249
274
 
250
275
  @jobscript.command(name="res")
251
- @click.pass_context
252
- def resources(ctx):
276
+ @_pass_js
277
+ def resources(job: Jobscript):
253
278
  """Get resources associated with this jobscript."""
254
- click.echo(ctx.obj["jobscript"].resources.__dict__)
279
+ click.echo(job.resources.__dict__)
255
280
 
256
281
  @jobscript.command(name="deps")
257
- @click.pass_context
258
- def dependencies(ctx):
282
+ @_pass_js
283
+ def dependencies(job: Jobscript):
259
284
  """Get jobscript dependencies."""
260
- click.echo(ctx.obj["jobscript"].dependencies)
285
+ click.echo(job.dependencies)
261
286
 
262
287
  @jobscript.command()
263
- @click.pass_context
264
- def path(ctx):
288
+ @_pass_js
289
+ def path(job: Jobscript):
265
290
  """Get the file path to the jobscript."""
266
- click.echo(ctx.obj["jobscript"].jobscript_path)
291
+ click.echo(job.jobscript_path)
267
292
 
268
293
  @jobscript.command()
269
- @click.pass_context
270
- def show(ctx):
294
+ @_pass_js
295
+ def show(job: Jobscript):
271
296
  """Show the jobscript file."""
272
- with ctx.obj["jobscript"].jobscript_path.open("rt") as fp:
297
+ with job.jobscript_path.open("rt") as fp:
273
298
  click.echo(fp.read())
274
299
 
275
- jobscript.help = jobscript.help.format(app_name=app.name)
276
-
300
+ _set_help_name(jobscript, app)
277
301
  return jobscript
278
302
 
279
303
 
280
- def _make_workflow_submission_CLI(app):
304
+ def _make_workflow_submission_CLI(app: BaseApp):
281
305
  """Generate the CLI for interacting with existing workflow submissions."""
282
306
 
283
307
  @click.group(name="sub")
308
+ @_pass_workflow
284
309
  @click.pass_context
285
310
  @click.argument("sub_idx", type=click.INT)
286
- def submission(ctx, sub_idx):
311
+ def submission(ctx: click.Context, wf: Workflow, sub_idx: int):
287
312
  """Interact with existing {app_name} workflow submissions.
288
313
 
289
314
  SUB_IDX is the submission index.
290
315
 
291
316
  """
292
- ctx.obj["submission"] = ctx.obj["workflow"].submissions[sub_idx]
317
+ ctx.obj = wf.submissions[sub_idx]
293
318
 
294
319
  @submission.command("status")
295
- @click.pass_context
296
- def status(ctx):
320
+ @_pass_submission
321
+ def status(sb: Submission):
297
322
  """Get the submission status."""
298
- click.echo(ctx.obj["submission"].status.name.lower())
323
+ click.echo(sb.status.name.lower())
299
324
 
300
325
  @submission.command("submitted-js")
301
- @click.pass_context
302
- def submitted_JS(ctx):
326
+ @_pass_submission
327
+ def submitted_JS(sb: Submission):
303
328
  """Get a list of jobscript indices that have been submitted."""
304
- click.echo(ctx.obj["submission"].submitted_jobscripts)
329
+ click.echo(sb.submitted_jobscripts)
305
330
 
306
331
  @submission.command("outstanding-js")
307
- @click.pass_context
308
- def outstanding_JS(ctx):
332
+ @_pass_submission
333
+ def outstanding_JS(sb: Submission):
309
334
  """Get a list of jobscript indices that have not yet been submitted."""
310
- click.echo(ctx.obj["submission"].outstanding_jobscripts)
335
+ click.echo(sb.outstanding_jobscripts)
311
336
 
312
337
  @submission.command("needs-submit")
313
- @click.pass_context
314
- def needs_submit(ctx):
338
+ @_pass_submission
339
+ def needs_submit(sb: Submission):
315
340
  """Check if this submission needs submitting."""
316
- click.echo(ctx.obj["submission"].needs_submit)
341
+ click.echo(sb.needs_submit)
317
342
 
318
343
  @submission.command("get-active-jobscripts")
319
- @click.pass_context
320
- def get_active_jobscripts(ctx):
344
+ @_pass_submission
345
+ def get_active_jobscripts(sb: Submission):
321
346
  """Show active jobscripts and their jobscript-element states."""
322
- pprint(ctx.obj["submission"].get_active_jobscripts(as_json=True))
347
+ pprint(sb.get_active_jobscripts(as_json=True))
323
348
 
324
- submission.help = submission.help.format(app_name=app.name)
349
+ _set_help_name(submission, app)
325
350
  submission.add_command(_make_workflow_submission_jobscript_CLI(app))
326
-
327
351
  return submission
328
352
 
329
353
 
330
- def _make_workflow_CLI(app):
354
+ def _make_workflow_CLI(app: BaseApp):
331
355
  """Generate the CLI for interacting with existing workflows."""
332
356
 
333
357
  @click.group()
334
358
  @click.argument("workflow_ref")
335
359
  @workflow_ref_type_opt
336
360
  @click.pass_context
337
- def workflow(ctx, workflow_ref, ref_type):
361
+ def workflow(ctx: click.Context, workflow_ref: str, ref_type: str | None):
338
362
  """Interact with existing {app_name} workflows.
339
363
 
340
364
  WORKFLOW_REF is the path to, or local ID of, an existing workflow.
341
365
 
342
366
  """
343
367
  workflow_path = app._resolve_workflow_reference(workflow_ref, ref_type)
344
- wk = app.Workflow(workflow_path)
345
- ctx.ensure_object(dict)
346
- ctx.obj["workflow"] = wk
368
+ ctx.obj = app.Workflow(workflow_path)
347
369
 
348
370
  @workflow.command(name="submit")
349
371
  @js_parallelism_option
@@ -353,23 +375,23 @@ def _make_workflow_CLI(app):
353
375
  @tasks_opt
354
376
  @cancel_opt
355
377
  @submit_status_opt
356
- @click.pass_context
378
+ @_pass_workflow
357
379
  def submit_workflow(
358
- ctx,
359
- js_parallelism=None,
360
- wait=False,
361
- add_to_known=True,
362
- print_idx=False,
363
- tasks=None,
364
- cancel=False,
365
- status=True,
380
+ wf: Workflow,
381
+ js_parallelism: bool | None = None,
382
+ wait: bool = False,
383
+ add_to_known: bool = True,
384
+ print_idx: bool = False,
385
+ tasks: list[int] | None = None,
386
+ cancel: bool = False,
387
+ status: bool = True,
366
388
  ):
367
389
  """Submit the workflow."""
368
- out = ctx.obj["workflow"].submit(
390
+ out = wf.submit(
369
391
  JS_parallelism=js_parallelism,
370
392
  wait=wait,
371
393
  add_to_known=add_to_known,
372
- return_idx=print_idx,
394
+ return_idx=True,
373
395
  tasks=tasks,
374
396
  cancel=cancel,
375
397
  status=status,
@@ -389,19 +411,19 @@ def _make_workflow_CLI(app):
389
411
  "separate patterns like these."
390
412
  ),
391
413
  )
392
- @click.pass_context
393
- def wait(ctx, jobscripts):
414
+ @_pass_workflow
415
+ def wait(wf: Workflow, jobscripts: str | None):
394
416
  js_spec = parse_jobscript_wait_spec(jobscripts) if jobscripts else None
395
- ctx.obj["workflow"].wait(sub_js=js_spec)
417
+ wf.wait(sub_js=js_spec)
396
418
 
397
419
  @workflow.command(name="abort-run")
398
420
  @click.option("--submission", type=click.INT, default=-1)
399
421
  @click.option("--task", type=click.INT)
400
422
  @click.option("--element", type=click.INT)
401
- @click.pass_context
402
- def abort_run(ctx, submission, task, element):
423
+ @_pass_workflow
424
+ def abort_run(wf: Workflow, submission: int, task: int, element: int):
403
425
  """Abort the specified run."""
404
- ctx.obj["workflow"].abort_run(
426
+ wf.abort_run(
405
427
  submission_idx=submission,
406
428
  task_idx=task,
407
429
  element_idx=element,
@@ -409,36 +431,36 @@ def _make_workflow_CLI(app):
409
431
 
410
432
  @workflow.command(name="get-param")
411
433
  @click.argument("index", type=click.INT)
412
- @click.pass_context
413
- def get_parameter(ctx, index):
434
+ @_pass_workflow
435
+ def get_parameter(wf: Workflow, index: int):
414
436
  """Get a parameter value by data index."""
415
- click.echo(ctx.obj["workflow"].get_parameter_data(index))
437
+ click.echo(wf.get_parameter_data(index))
416
438
 
417
439
  @workflow.command(name="get-param-source")
418
440
  @click.argument("index", type=click.INT)
419
- @click.pass_context
420
- def get_parameter_source(ctx, index):
441
+ @_pass_workflow
442
+ def get_parameter_source(wf: Workflow, index: int):
421
443
  """Get a parameter source by data index."""
422
- click.echo(ctx.obj["workflow"].get_parameter_source(index))
444
+ click.echo(wf.get_parameter_source(index))
423
445
 
424
446
  @workflow.command(name="get-all-params")
425
- @click.pass_context
426
- def get_all_parameters(ctx):
447
+ @_pass_workflow
448
+ def get_all_parameters(wf: Workflow):
427
449
  """Get all parameter values."""
428
- click.echo(ctx.obj["workflow"].get_all_parameter_data())
450
+ click.echo(wf.get_all_parameter_data())
429
451
 
430
452
  @workflow.command(name="is-param-set")
431
453
  @click.argument("index", type=click.INT)
432
- @click.pass_context
433
- def is_parameter_set(ctx, index):
454
+ @_pass_workflow
455
+ def is_parameter_set(wf: Workflow, index: int):
434
456
  """Check if a parameter specified by data index is set."""
435
- click.echo(ctx.obj["workflow"].is_parameter_set(index))
457
+ click.echo(wf.is_parameter_set(index))
436
458
 
437
459
  @workflow.command(name="show-all-status")
438
- @click.pass_context
439
- def show_all_EAR_statuses(ctx):
460
+ @_pass_workflow
461
+ def show_all_EAR_statuses(wf: Workflow):
440
462
  """Show the submission status of all workflow EARs."""
441
- ctx.obj["workflow"].show_all_EAR_statuses()
463
+ wf.show_all_EAR_statuses()
442
464
 
443
465
  @workflow.command(name="zip")
444
466
  @zip_path_opt
@@ -446,19 +468,19 @@ def _make_workflow_CLI(app):
446
468
  @zip_log_opt
447
469
  @zip_include_execute_opt
448
470
  @zip_include_rechunk_backups_opt
449
- @click.pass_context
471
+ @_pass_workflow
450
472
  def zip_workflow(
451
- ctx,
452
- path,
453
- overwrite,
454
- log,
455
- include_execute,
456
- include_rechunk_backups,
473
+ wf: Workflow,
474
+ path: str,
475
+ overwrite: bool,
476
+ log: str | None,
477
+ include_execute: bool,
478
+ include_rechunk_backups: bool,
457
479
  ):
458
480
  """Generate a copy of the workflow in the zip file format in the current working
459
481
  directory."""
460
482
  click.echo(
461
- ctx.obj["workflow"].zip(
483
+ wf.zip(
462
484
  path=path,
463
485
  overwrite=overwrite,
464
486
  log=log,
@@ -470,54 +492,48 @@ def _make_workflow_CLI(app):
470
492
  @workflow.command(name="unzip")
471
493
  @unzip_path_opt
472
494
  @unzip_log_opt
473
- @click.pass_context
474
- def unzip_workflow(ctx, path, log):
495
+ @_pass_workflow
496
+ def unzip_workflow(wf: Workflow, path: str, log: str | None):
475
497
  """Generate a copy of the zipped workflow in the submittable Zarr format in the
476
498
  current working directory."""
477
- click.echo(ctx.obj["workflow"].unzip(path=path, log=log))
499
+ click.echo(wf.unzip(path=path, log=log))
478
500
 
479
501
  @workflow.command(name="rechunk")
480
502
  @rechunk_backup_opt
481
503
  @rechunk_chunk_size_opt
482
504
  @rechunk_status_opt
483
- @click.pass_context
484
- def rechunk(ctx, backup, chunk_size, status):
505
+ @_pass_workflow
506
+ def rechunk(wf: Workflow, backup: bool, chunk_size: int, status: bool):
485
507
  """Rechunk metadata/runs and parameters/base arrays."""
486
- ctx.obj["workflow"].rechunk(backup=backup, chunk_size=chunk_size, status=status)
508
+ wf.rechunk(backup=backup, chunk_size=chunk_size, status=status)
487
509
 
488
510
  @workflow.command(name="rechunk-runs")
489
511
  @rechunk_backup_opt
490
512
  @rechunk_chunk_size_opt
491
513
  @rechunk_status_opt
492
- @click.pass_context
493
- def rechunk_runs(ctx, backup, chunk_size, status):
514
+ @_pass_workflow
515
+ def rechunk_runs(wf: Workflow, backup: bool, chunk_size: int, status: bool):
494
516
  """Rechunk the metadata/runs array."""
495
- ctx.obj["workflow"].rechunk_runs(
496
- backup=backup, chunk_size=chunk_size, status=status
497
- )
517
+ wf.rechunk_runs(backup=backup, chunk_size=chunk_size, status=status)
498
518
 
499
519
  @workflow.command(name="rechunk-parameter-base")
500
520
  @rechunk_backup_opt
501
521
  @rechunk_chunk_size_opt
502
522
  @rechunk_status_opt
503
- @click.pass_context
504
- def rechunk_parameter_base(ctx, backup, chunk_size, status):
523
+ @_pass_workflow
524
+ def rechunk_parameter_base(wf: Workflow, backup: bool, chunk_size: int, status: bool):
505
525
  """Rechunk the parameters/base array."""
506
- ctx.obj["workflow"].rechunk_parameter_base(
507
- backup=backup, chunk_size=chunk_size, status=status
508
- )
509
-
510
- workflow.help = workflow.help.format(app_name=app.name)
526
+ wf.rechunk_parameter_base(backup=backup, chunk_size=chunk_size, status=status)
511
527
 
528
+ _set_help_name(workflow, app)
512
529
  workflow.add_command(_make_workflow_submission_CLI(app))
513
-
514
530
  return workflow
515
531
 
516
532
 
517
- def _make_submission_CLI(app):
533
+ def _make_submission_CLI(app: BaseApp):
518
534
  """Generate the CLI for submission related queries."""
519
535
 
520
- def OS_info_callback(ctx, param, value):
536
+ def OS_info_callback(ctx: click.Context, param, value: bool):
521
537
  if not value or ctx.resilient_parsing:
522
538
  return
523
539
  pprint(app.get_OS_info())
@@ -532,16 +548,15 @@ def _make_submission_CLI(app):
532
548
  expose_value=False,
533
549
  callback=OS_info_callback,
534
550
  )
535
- @click.pass_context
536
- def submission(ctx):
551
+ def submission():
537
552
  """Submission-related queries."""
538
- ctx.ensure_object(dict)
553
+ pass
539
554
 
540
555
  @submission.command("shell-info")
541
- @click.argument("shell_name", type=click.Choice(ALL_SHELLS))
556
+ @click.argument("shell_name", type=click.Choice(list(ALL_SHELLS)))
542
557
  @click.option("--exclude-os", is_flag=True, default=False)
543
558
  @click.pass_context
544
- def shell_info(ctx, shell_name, exclude_os):
559
+ def shell_info(ctx: click.Context, shell_name: str, exclude_os: bool):
545
560
  """Show information about the specified shell, such as the version."""
546
561
  pprint(app.get_shell_info(shell_name, exclude_os))
547
562
  ctx.exit()
@@ -549,13 +564,14 @@ def _make_submission_CLI(app):
549
564
  @submission.group("scheduler")
550
565
  @click.argument("scheduler_name")
551
566
  @click.pass_context
552
- def scheduler(ctx, scheduler_name):
553
- ctx.obj["scheduler_obj"] = app.get_scheduler(scheduler_name, os.name)
567
+ def scheduler(ctx: click.Context, scheduler_name: str):
568
+ ctx.obj = app.get_scheduler(scheduler_name, os.name)
569
+
570
+ pass_scheduler = click.make_pass_decorator(SGEPosix)
554
571
 
555
572
  @scheduler.command()
556
- @click.pass_context
557
- def get_login_nodes(ctx):
558
- scheduler = ctx.obj["scheduler_obj"]
573
+ @pass_scheduler
574
+ def get_login_nodes(scheduler: SGEPosix):
559
575
  pprint(scheduler.get_login_nodes())
560
576
 
561
577
  @submission.command()
@@ -566,8 +582,7 @@ def _make_submission_CLI(app):
566
582
  default=False,
567
583
  help="Do not format and only show JSON-compatible information.",
568
584
  )
569
- @click.pass_context
570
- def get_known(ctx, as_json=False):
585
+ def get_known(as_json: bool = False):
571
586
  """Print known-submissions information as a formatted Python object."""
572
587
  out = app.get_known_submissions(as_json=as_json)
573
588
  if as_json:
@@ -578,11 +593,11 @@ def _make_submission_CLI(app):
578
593
  return submission
579
594
 
580
595
 
581
- def _make_internal_CLI(app):
596
+ def _make_internal_CLI(app: BaseApp):
582
597
  """Generate the CLI for internal use."""
583
598
 
584
599
  @click.group()
585
- def internal(help=True): # TEMP
600
+ def internal(help: bool = True): # TEMP
586
601
  """Internal CLI to be invoked by scripts generated by the app."""
587
602
  pass
588
603
 
@@ -594,36 +609,36 @@ def _make_internal_CLI(app):
594
609
  @internal.group()
595
610
  @click.argument("path", type=click.Path(exists=True))
596
611
  @click.pass_context
597
- def workflow(ctx, path):
612
+ def workflow(ctx: click.Context, path: Path):
598
613
  """"""
599
- wk = app.Workflow(path)
600
- ctx.ensure_object(dict)
601
- ctx.obj["workflow"] = wk
614
+ ctx.obj = app.Workflow(path)
602
615
 
603
616
  @workflow.command()
617
+ @_pass_workflow
604
618
  @click.pass_context
605
619
  @click.argument("submission_idx", type=click.INT)
606
620
  @click.argument("jobscript_idx", type=click.INT)
607
621
  @click.argument("js_action_idx", type=click.INT)
608
622
  @click.argument("ear_id", type=click.INT)
609
623
  def write_commands(
610
- ctx,
624
+ ctx: click.Context,
625
+ wf: Workflow,
611
626
  submission_idx: int,
612
627
  jobscript_idx: int,
613
628
  js_action_idx: int,
614
629
  ear_id: int,
615
630
  ):
616
631
  app.CLI_logger.info(f"write commands for EAR ID {ear_id!r}.")
617
- ctx.exit(
618
- ctx.obj["workflow"].write_commands(
619
- submission_idx,
620
- jobscript_idx,
621
- js_action_idx,
622
- ear_id,
623
- )
632
+ wf.write_commands(
633
+ submission_idx,
634
+ jobscript_idx,
635
+ js_action_idx,
636
+ ear_id,
624
637
  )
638
+ ctx.exit()
625
639
 
626
640
  @workflow.command()
641
+ @_pass_workflow
627
642
  @click.pass_context
628
643
  @click.argument("name")
629
644
  @click.argument("value")
@@ -631,7 +646,8 @@ def _make_internal_CLI(app):
631
646
  @click.argument("cmd_idx", type=click.INT)
632
647
  @click.option("--stderr", is_flag=True, default=False)
633
648
  def save_parameter(
634
- ctx,
649
+ ctx: click.Context,
650
+ wf: Workflow,
635
651
  name: str,
636
652
  value: str,
637
653
  ear_id: int,
@@ -643,9 +659,8 @@ def _make_internal_CLI(app):
643
659
  f"{cmd_idx!r} (stderr={stderr!r})"
644
660
  )
645
661
  app.CLI_logger.debug(f"save parameter value is: {value!r}")
646
- wk = ctx.obj["workflow"]
647
- with wk._store.cached_load():
648
- value = wk.process_shell_parameter_output(
662
+ with wf._store.cached_load():
663
+ value = wf.process_shell_parameter_output(
649
664
  name=name,
650
665
  value=value,
651
666
  EAR_ID=ear_id,
@@ -653,23 +668,27 @@ def _make_internal_CLI(app):
653
668
  stderr=stderr,
654
669
  )
655
670
  app.CLI_logger.debug(f"save parameter processed value is: {value!r}")
656
- ctx.exit(wk.save_parameter(name=name, value=value, EAR_ID=ear_id))
671
+ ctx.exit(wf.save_parameter(name=name, value=value, EAR_ID=ear_id))
657
672
 
658
673
  @workflow.command()
674
+ @_pass_workflow
659
675
  @click.pass_context
660
676
  @click.argument("ear_id", type=click.INT)
661
- def set_EAR_start(ctx, ear_id: int):
677
+ def set_EAR_start(ctx: click.Context, wf: Workflow, ear_id: int):
662
678
  app.CLI_logger.info(f"set EAR start for EAR ID {ear_id!r}.")
663
- ctx.exit(ctx.obj["workflow"].set_EAR_start(ear_id))
679
+ wf.set_EAR_start(ear_id)
680
+ ctx.exit()
664
681
 
665
682
  @workflow.command()
683
+ @_pass_workflow
666
684
  @click.pass_context
667
685
  @click.argument("js_idx", type=click.INT)
668
686
  @click.argument("js_act_idx", type=click.INT)
669
687
  @click.argument("ear_id", type=click.INT)
670
688
  @click.argument("exit_code", type=click.INT)
671
689
  def set_EAR_end(
672
- ctx,
690
+ ctx: click.Context,
691
+ wf: Workflow,
673
692
  js_idx: int,
674
693
  js_act_idx: int,
675
694
  ear_id: int,
@@ -678,38 +697,42 @@ def _make_internal_CLI(app):
678
697
  app.CLI_logger.info(
679
698
  f"set EAR end for EAR ID {ear_id!r} with exit code {exit_code!r}."
680
699
  )
681
- ctx.exit(
682
- ctx.obj["workflow"].set_EAR_end(
683
- js_idx=js_idx,
684
- js_act_idx=js_act_idx,
685
- EAR_ID=ear_id,
686
- exit_code=exit_code,
687
- )
700
+ wf.set_EAR_end(
701
+ js_idx=js_idx,
702
+ js_act_idx=js_act_idx,
703
+ EAR_ID=ear_id,
704
+ exit_code=exit_code,
688
705
  )
706
+ ctx.exit()
689
707
 
690
708
  @workflow.command()
709
+ @_pass_workflow
691
710
  @click.pass_context
692
711
  @click.argument("ear_id", type=click.INT)
693
- def set_EAR_skip(ctx, ear_id: int):
712
+ def set_EAR_skip(ctx: click.Context, wf: Workflow, ear_id: int):
694
713
  app.CLI_logger.info(f"set EAR skip for EAR ID {ear_id!r}.")
695
- ctx.exit(ctx.obj["workflow"].set_EAR_skip(ear_id))
714
+ wf.set_EAR_skip(ear_id)
715
+ ctx.exit()
696
716
 
697
717
  @workflow.command()
718
+ @_pass_workflow
698
719
  @click.pass_context
699
720
  @click.argument("ear_id", type=click.INT)
700
- def get_EAR_skipped(ctx, ear_id: int):
721
+ def get_EAR_skipped(ctx: click.Context, wf: Workflow, ear_id: int):
701
722
  """Return 1 if the given EAR is to be skipped, else return 0."""
702
723
  app.CLI_logger.info(f"get EAR skip for EAR ID {ear_id!r}.")
703
- click.echo(int(ctx.obj["workflow"].get_EAR_skipped(ear_id)))
724
+ click.echo(int(wf.get_EAR_skipped(ear_id)))
704
725
 
705
726
  @workflow.command()
727
+ @_pass_workflow
706
728
  @click.pass_context
707
729
  @click.argument("loop_name", type=click.STRING)
708
730
  @click.argument("ear_id", type=click.INT)
709
- def check_loop(ctx, loop_name: str, ear_id: int):
731
+ def check_loop(ctx: click.Context, wf: Workflow, loop_name: str, ear_id: int):
710
732
  """Check if an iteration has met its loop's termination condition."""
711
733
  app.CLI_logger.info(f"check_loop for loop {loop_name!r} and EAR ID {ear_id!r}.")
712
- ctx.exit(ctx.obj["workflow"].check_loop_termination(loop_name, ear_id))
734
+ wf.check_loop_termination(loop_name, ear_id)
735
+ ctx.exit()
713
736
 
714
737
  # TODO: in general, maybe the workflow command group can expose the simple Workflow
715
738
  # properties; maybe use a decorator on the Workflow property object to signify
@@ -718,17 +741,17 @@ def _make_internal_CLI(app):
718
741
  return internal
719
742
 
720
743
 
721
- def _make_template_components_CLI(app):
744
+ def _make_template_components_CLI(app: BaseApp):
722
745
  @click.command()
723
- def tc(help=True):
746
+ def tc(help: bool = True):
724
747
  """For showing template component data."""
725
748
  pprint(app.template_components)
726
749
 
727
750
  return tc
728
751
 
729
752
 
730
- def _make_show_CLI(app):
731
- def show_legend_callback(ctx, param, value):
753
+ def _make_show_CLI(app: BaseApp):
754
+ def show_legend_callback(ctx: click.Context, param, value: bool):
732
755
  if not value or ctx.resilient_parsing:
733
756
  return
734
757
  app.show_legend()
@@ -765,14 +788,14 @@ def _make_show_CLI(app):
765
788
  expose_value=False,
766
789
  callback=show_legend_callback,
767
790
  )
768
- def show(max_recent, full, no_update):
791
+ def show(max_recent: int, full: bool, no_update: bool):
769
792
  """Show information about running and recently active workflows."""
770
793
  app.show(max_recent=max_recent, full=full, no_update=no_update)
771
794
 
772
795
  return show
773
796
 
774
797
 
775
- def _make_zip_CLI(app):
798
+ def _make_zip_CLI(app: BaseApp):
776
799
  @click.command(name="zip")
777
800
  @click.argument("workflow_ref")
778
801
  @zip_path_opt
@@ -782,13 +805,13 @@ def _make_zip_CLI(app):
782
805
  @zip_include_rechunk_backups_opt
783
806
  @workflow_ref_type_opt
784
807
  def zip_workflow(
785
- workflow_ref,
786
- path,
787
- overwrite,
788
- log,
789
- include_execute,
790
- include_rechunk_backups,
791
- ref_type,
808
+ workflow_ref: str,
809
+ path: str,
810
+ overwrite: bool,
811
+ log: str | None,
812
+ include_execute: bool,
813
+ include_rechunk_backups: bool,
814
+ ref_type: str | None,
792
815
  ):
793
816
  """Generate a copy of the specified workflow in the zip file format in the
794
817
  current working directory.
@@ -811,12 +834,12 @@ def _make_zip_CLI(app):
811
834
  return zip_workflow
812
835
 
813
836
 
814
- def _make_unzip_CLI(app):
837
+ def _make_unzip_CLI(app: BaseApp):
815
838
  @click.command(name="unzip")
816
839
  @click.argument("workflow_path")
817
840
  @unzip_path_opt
818
841
  @unzip_log_opt
819
- def unzip_workflow(workflow_path, path, log):
842
+ def unzip_workflow(workflow_path: str, path: str, log: str | None):
820
843
  """Generate a copy of the specified zipped workflow in the submittable Zarr
821
844
  format in the current working directory.
822
845
 
@@ -829,11 +852,11 @@ def _make_unzip_CLI(app):
829
852
  return unzip_workflow
830
853
 
831
854
 
832
- def _make_cancel_CLI(app):
855
+ def _make_cancel_CLI(app: BaseApp):
833
856
  @click.command()
834
857
  @click.argument("workflow_ref")
835
858
  @workflow_ref_type_opt
836
- def cancel(workflow_ref, ref_type):
859
+ def cancel(workflow_ref: str, ref_type: str | None):
837
860
  """Stop all running jobscripts of the specified workflow.
838
861
 
839
862
  WORKFLOW_REF is the local ID (that provided by the `show` command}) or the
@@ -845,14 +868,20 @@ def _make_cancel_CLI(app):
845
868
  return cancel
846
869
 
847
870
 
848
- def _make_rechunk_CLI(app):
871
+ def _make_rechunk_CLI(app: BaseApp):
849
872
  @click.command(name="rechunk")
850
873
  @click.argument("workflow_ref")
851
874
  @workflow_ref_type_opt
852
875
  @rechunk_backup_opt
853
876
  @rechunk_chunk_size_opt
854
877
  @rechunk_status_opt
855
- def rechunk(workflow_ref, ref_type, backup, chunk_size, status):
878
+ def rechunk(
879
+ workflow_ref: str,
880
+ ref_type: str | None,
881
+ backup: bool,
882
+ chunk_size: int,
883
+ status: bool,
884
+ ):
856
885
  """Rechunk metadata/runs and parameters/base arrays.
857
886
 
858
887
  WORKFLOW_REF is the local ID (that provided by the `show` command}) or the
@@ -866,7 +895,7 @@ def _make_rechunk_CLI(app):
866
895
  return rechunk
867
896
 
868
897
 
869
- def _make_open_CLI(app):
898
+ def _make_open_CLI(app: BaseApp):
870
899
  @click.group(name="open")
871
900
  def open_file():
872
901
  """Open a file (for example {app_name}'s log file) using the default
@@ -874,9 +903,9 @@ def _make_open_CLI(app):
874
903
 
875
904
  @open_file.command()
876
905
  @click.option("--path", is_flag=True, default=False)
877
- def log(path=False):
906
+ def log(path: bool = False):
878
907
  """Open the {app_name} log file."""
879
- file_path = app.config.get("log_file_path")
908
+ file_path = app.config.log_file_path
880
909
  if path:
881
910
  click.echo(file_path)
882
911
  else:
@@ -884,9 +913,9 @@ def _make_open_CLI(app):
884
913
 
885
914
  @open_file.command()
886
915
  @click.option("--path", is_flag=True, default=False)
887
- def config(path=False):
916
+ def config(path: bool = False):
888
917
  """Open the {app_name} config file, or retrieve it's path."""
889
- file_path = app.config.get("config_file_path")
918
+ file_path = app.config.config_file_path
890
919
  if path:
891
920
  click.echo(file_path)
892
921
  else:
@@ -895,34 +924,30 @@ def _make_open_CLI(app):
895
924
  @open_file.command()
896
925
  @click.option("--name")
897
926
  @click.option("--path", is_flag=True, default=False)
898
- def env_source(name=None, path=False):
927
+ def env_source(name: str | None = None, path: bool = False):
899
928
  """Open a named environment sources file, or the first one."""
900
- sources = app.config.get("environment_sources")
901
- if not sources:
929
+ if not (sources := app.config.environment_sources):
902
930
  raise ValueError("No environment sources specified in the config file.")
903
- file_paths = []
904
931
  if not name:
905
932
  file_paths = [sources[0]]
906
933
  else:
907
- for i in sources:
908
- if i.name == name:
909
- file_paths.append(i)
934
+ file_paths = [pth for pth in sources if pth.name == name]
910
935
  if not file_paths:
911
936
  raise ValueError(
912
937
  f"No environment source named {name!r} could be found; available "
913
- f"environment source files have names: {[i.name for i in sources]!r}"
938
+ f"environment source files have names: {[pth.name for pth in sources]!r}"
914
939
  )
915
940
 
916
941
  assert len(file_paths) < 5 # don't open a stupid number of files
917
- for i in file_paths:
942
+ for pth in file_paths:
918
943
  if path:
919
- click.echo(i)
944
+ click.echo(pth)
920
945
  else:
921
- utils.open_file(i)
946
+ utils.open_file(pth)
922
947
 
923
948
  @open_file.command()
924
949
  @click.option("--path", is_flag=True, default=False)
925
- def known_subs(path=False):
950
+ def known_subs(path: bool = False):
926
951
  """Open the known-submissions text file."""
927
952
  file_path = app.known_subs_file_path
928
953
  if path:
@@ -934,7 +959,7 @@ def _make_open_CLI(app):
934
959
  @click.argument("workflow_ref")
935
960
  @click.option("--path", is_flag=True, default=False)
936
961
  @workflow_ref_type_opt
937
- def workflow(workflow_ref, ref_type, path=False):
962
+ def workflow(workflow_ref: str, ref_type: str | None, path: bool = False):
938
963
  """Open a workflow directory using, for example, File Explorer on Windows."""
939
964
  workflow_path = app._resolve_workflow_reference(workflow_ref, ref_type)
940
965
  if path:
@@ -944,7 +969,7 @@ def _make_open_CLI(app):
944
969
 
945
970
  @open_file.command()
946
971
  @click.option("--path", is_flag=True, default=False)
947
- def user_data_dir(path=False):
972
+ def user_data_dir(path: bool = False):
948
973
  dir_path = app._ensure_user_data_dir()
949
974
  if path:
950
975
  click.echo(dir_path)
@@ -953,7 +978,7 @@ def _make_open_CLI(app):
953
978
 
954
979
  @open_file.command()
955
980
  @click.option("--path", is_flag=True, default=False)
956
- def user_cache_dir(path=False):
981
+ def user_cache_dir(path: bool = False):
957
982
  dir_path = app._ensure_user_cache_dir()
958
983
  if path:
959
984
  click.echo(dir_path)
@@ -962,7 +987,7 @@ def _make_open_CLI(app):
962
987
 
963
988
  @open_file.command()
964
989
  @click.option("--path", is_flag=True, default=False)
965
- def user_runtime_dir(path=False):
990
+ def user_runtime_dir(path: bool = False):
966
991
  dir_path = app._ensure_user_runtime_dir()
967
992
  if path:
968
993
  click.echo(dir_path)
@@ -971,7 +996,7 @@ def _make_open_CLI(app):
971
996
 
972
997
  @open_file.command()
973
998
  @click.option("--path", is_flag=True, default=False)
974
- def user_data_hostname_dir(path=False):
999
+ def user_data_hostname_dir(path: bool = False):
975
1000
  dir_path = app._ensure_user_data_hostname_dir()
976
1001
  if path:
977
1002
  click.echo(dir_path)
@@ -980,7 +1005,7 @@ def _make_open_CLI(app):
980
1005
 
981
1006
  @open_file.command()
982
1007
  @click.option("--path", is_flag=True, default=False)
983
- def user_cache_hostname_dir(path=False):
1008
+ def user_cache_hostname_dir(path: bool = False):
984
1009
  dir_path = app._ensure_user_cache_hostname_dir()
985
1010
  if path:
986
1011
  click.echo(dir_path)
@@ -989,32 +1014,31 @@ def _make_open_CLI(app):
989
1014
 
990
1015
  @open_file.command()
991
1016
  @click.option("--path", is_flag=True, default=False)
992
- def demo_data_cache_dir(path=False):
1017
+ def demo_data_cache_dir(path: bool = False):
993
1018
  dir_path = app._ensure_demo_data_cache_dir()
994
1019
  if path:
995
1020
  click.echo(dir_path)
996
1021
  else:
997
1022
  utils.open_file(dir_path)
998
1023
 
999
- open_file.help = open_file.help.format(app_name=app.name)
1000
- log.help = log.help.format(app_name=app.name)
1001
- config.help = config.help.format(app_name=app.name)
1002
-
1024
+ _set_help_name(open_file, app)
1025
+ _set_help_name(log, app)
1026
+ _set_help_name(config, app)
1003
1027
  return open_file
1004
1028
 
1005
1029
 
1006
- def _make_demo_data_CLI(app):
1030
+ def _make_demo_data_CLI(app: BaseApp):
1007
1031
  """Generate the CLI for interacting with example data files that are used in demo
1008
1032
  workflows."""
1009
1033
 
1010
- def list_callback(ctx, param, value):
1034
+ def list_callback(ctx: click.Context, param, value: bool):
1011
1035
  if not value or ctx.resilient_parsing:
1012
1036
  return
1013
1037
  # TODO: format with Rich with a one-line description
1014
1038
  click.echo("\n".join(app.list_demo_data_files()))
1015
1039
  ctx.exit()
1016
1040
 
1017
- def cache_all_callback(ctx, param, value):
1041
+ def cache_all_callback(ctx: click.Context, param, value: bool):
1018
1042
  if not value or ctx.resilient_parsing:
1019
1043
  return
1020
1044
  app.cache_all_demo_data_files()
@@ -1036,7 +1060,7 @@ def _make_demo_data_CLI(app):
1036
1060
  @demo_data.command("copy")
1037
1061
  @click.argument("file_name")
1038
1062
  @click.argument("destination")
1039
- def copy_demo_data(file_name, destination):
1063
+ def copy_demo_data(file_name: str, destination: str):
1040
1064
  """Copy a demo data file to the specified location."""
1041
1065
  app.copy_demo_data(file_name=file_name, dst=destination)
1042
1066
 
@@ -1050,14 +1074,14 @@ def _make_demo_data_CLI(app):
1050
1074
  callback=cache_all_callback,
1051
1075
  )
1052
1076
  @click.argument("file_name")
1053
- def cache_demo_data(file_name):
1077
+ def cache_demo_data(file_name: str):
1054
1078
  """Ensure a demo data file is in the demo data cache."""
1055
1079
  app.cache_demo_data_file(file_name)
1056
1080
 
1057
1081
  return demo_data
1058
1082
 
1059
1083
 
1060
- def _make_manage_CLI(app):
1084
+ def _make_manage_CLI(app: BaseApp):
1061
1085
  """Generate the CLI for infrequent app management tasks."""
1062
1086
 
1063
1087
  @click.group()
@@ -1074,7 +1098,7 @@ def _make_manage_CLI(app):
1074
1098
  "--config-dir",
1075
1099
  help="The directory containing the config file to be reset.",
1076
1100
  )
1077
- def reset_config(config_dir):
1101
+ def reset_config(config_dir: str):
1078
1102
  """Reset the configuration file to defaults.
1079
1103
 
1080
1104
  This can be used if the current configuration file is invalid."""
@@ -1085,7 +1109,7 @@ def _make_manage_CLI(app):
1085
1109
  "--config-dir",
1086
1110
  help="The directory containing the config file whose path is to be returned.",
1087
1111
  )
1088
- def get_config_path(config_dir):
1112
+ def get_config_path(config_dir: str):
1089
1113
  """Print the config file path without loading the config.
1090
1114
 
1091
1115
  This can be used instead of `{app_name} open config --path` if the config file
@@ -1106,7 +1130,7 @@ def _make_manage_CLI(app):
1106
1130
 
1107
1131
  @manage.command("clear-cache")
1108
1132
  @click.option("--hostname", is_flag=True, default=False)
1109
- def clear_cache(hostname):
1133
+ def clear_cache(hostname: bool):
1110
1134
  """Delete the app cache directory."""
1111
1135
  if hostname:
1112
1136
  app.clear_user_cache_hostname_dir()
@@ -1121,12 +1145,12 @@ def _make_manage_CLI(app):
1121
1145
  return manage
1122
1146
 
1123
1147
 
1124
- def make_cli(app):
1148
+ def make_cli(app: BaseApp):
1125
1149
  """Generate the root CLI for the app."""
1126
1150
 
1127
1151
  colorama_init(autoreset=True)
1128
1152
 
1129
- def run_time_info_callback(ctx, param, value):
1153
+ def run_time_info_callback(ctx: click.Context, param, value: bool):
1130
1154
  app.run_time_info.from_CLI = True
1131
1155
  if not value or ctx.resilient_parsing:
1132
1156
  return
@@ -1181,7 +1205,9 @@ def make_cli(app):
1181
1205
  ),
1182
1206
  )
1183
1207
  @click.pass_context
1184
- def new_CLI(ctx, config_dir, config_key, with_config, timeit, timeit_file):
1208
+ def new_CLI(
1209
+ ctx: click.Context, config_dir, config_key, with_config, timeit, timeit_file
1210
+ ):
1185
1211
  app.run_time_info.from_CLI = True
1186
1212
  TimeIt.active = timeit or timeit_file
1187
1213
  TimeIt.file_path = timeit_file
@@ -1208,7 +1234,12 @@ def make_cli(app):
1208
1234
  @click.option("--use-current-env", is_flag=True, default=False)
1209
1235
  @click.option("--setup", type=click.STRING)
1210
1236
  @click.option("--env-source-file", type=click.STRING)
1211
- def configure_env(name, use_current_env, setup=None, env_source_file=None):
1237
+ def configure_env(
1238
+ name: str,
1239
+ use_current_env: bool,
1240
+ setup: list[str] | None = None,
1241
+ env_source_file: str | None = None,
1242
+ ):
1212
1243
  """Configure an app environment, using, for example, the currently activated
1213
1244
  Python environment."""
1214
1245
  app.configure_env(
@@ -1216,7 +1247,7 @@ def make_cli(app):
1216
1247
  setup=setup,
1217
1248
  executables=None,
1218
1249
  use_current_env=use_current_env,
1219
- env_source_file=env_source_file,
1250
+ env_source_file=None if env_source_file is None else Path(env_source_file),
1220
1251
  )
1221
1252
 
1222
1253
  new_CLI.__doc__ = app.description