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
@@ -1,16 +1,21 @@
1
+ from __future__ import annotations
2
+ from typing import cast, TYPE_CHECKING
1
3
  from hpcflow.app import app as hf
2
4
 
5
+ if TYPE_CHECKING:
6
+ from hpcflow.sdk.core.types import RuleArgs
3
7
 
4
- def test_equivalent_init_with_rule_args():
5
- rule_args = {
8
+
9
+ def test_equivalent_init_with_rule_args() -> None:
10
+ rule_args: RuleArgs = {
6
11
  "path": "resources.os_name",
7
12
  "condition": {"value.equal_to": "posix"},
8
13
  }
9
14
  assert hf.ActionRule(rule=hf.Rule(**rule_args)) == hf.ActionRule(**rule_args)
10
15
 
11
16
 
12
- def test_equivalent_init_json_like_with_rule_args():
13
- rule_args = {
17
+ def test_equivalent_init_json_like_with_rule_args() -> None:
18
+ rule_args: dict = {
14
19
  "path": "resources.os_name",
15
20
  "condition": {"value.equal_to": "posix"},
16
21
  }
@@ -1,37 +1,45 @@
1
+ from __future__ import annotations
2
+ from pathlib import Path
1
3
  import sys
4
+ from typing import TYPE_CHECKING
2
5
  import pytest
3
6
  import requests
4
7
 
5
8
  from hpcflow.app import app as hf
6
9
 
10
+ if TYPE_CHECKING:
11
+ from hpcflow.sdk.core.actions import Action, ActionEnvironment
12
+
7
13
 
8
14
  @pytest.fixture
9
- def act_env_1():
15
+ def act_env_1() -> ActionEnvironment:
10
16
  return hf.ActionEnvironment(environment="env_1")
11
17
 
12
18
 
13
19
  @pytest.fixture
14
- def act_1(act_env_1):
20
+ def act_1(act_env_1) -> Action:
15
21
  return hf.Action(
16
22
  commands=[hf.Command("<<parameter:p1>>")],
17
23
  environments=[act_env_1],
18
24
  )
19
25
 
20
26
 
21
- def test_shared_data_from_json_like_with_shared_data_dependency(act_1):
27
+ def test_shared_data_from_json_like_with_shared_data_dependency(act_1: Action):
22
28
  """Check we can generate some shared data objects where one depends on another."""
23
29
 
24
30
  p1 = hf.Parameter("p1")
25
31
  p1._set_hash()
26
32
  p1_hash = p1._hash_value
33
+ assert p1_hash is not None
27
34
 
28
35
  ts1 = hf.TaskSchema(objective="ts1", actions=[act_1], inputs=[p1])
29
36
  ts1._set_hash()
30
37
  ts1_hash = ts1._hash_value
38
+ assert ts1_hash is not None
31
39
 
32
40
  env_label = ts1.actions[0].environments[0].environment
33
41
 
34
- shared_data_json = {
42
+ shared_data_json: dict[str, dict] = {
35
43
  "parameters": {
36
44
  p1_hash: {
37
45
  "is_file": p1.is_file,
@@ -85,7 +93,7 @@ def test_shared_data_from_json_like_with_shared_data_dependency(act_1):
85
93
  ] == hf.TaskSchemasList([ts1])
86
94
 
87
95
 
88
- def test_get_demo_data_manifest(null_config):
96
+ def test_get_demo_data_manifest(null_config) -> None:
89
97
  hf.get_demo_data_files_manifest()
90
98
 
91
99
 
@@ -97,9 +105,28 @@ def test_get_demo_data_manifest(null_config):
97
105
  "retrieving demo data from GitHub."
98
106
  ),
99
107
  )
100
- def test_get_demo_data_cache(null_config):
108
+ def test_get_demo_data_cache(null_config) -> None:
101
109
  hf.clear_demo_data_cache_dir()
102
110
  hf.cache_demo_data_file("text_file.txt")
103
111
  with hf.demo_data_cache_dir.joinpath("text_file.txt").open("rt") as fh:
104
112
  contents = fh.read()
105
113
  assert contents == "\n".join(f"{i}" for i in range(1, 11)) + "\n"
114
+
115
+
116
+ def test_list_demo_workflows():
117
+ # sanity checks
118
+ lst = hf.list_demo_workflows()
119
+ assert isinstance(lst, tuple)
120
+ assert all(isinstance(i, str) and "." not in i for i in lst) # no extension included
121
+
122
+
123
+ def test_get_demo_workflows():
124
+ # sanity checks
125
+ lst = hf.list_demo_workflows()
126
+ demo_paths = hf._get_demo_workflows()
127
+ # keys should be those in the list:
128
+ assert sorted(list(lst)) == sorted(list(demo_paths.keys()))
129
+
130
+ # values should be distinct, absolute paths:
131
+ assert all(isinstance(i, Path) and i.is_absolute() for i in demo_paths.values())
132
+ assert len(set(demo_paths.values())) == len(demo_paths)
@@ -0,0 +1,46 @@
1
+ from pathlib import Path
2
+ from hpcflow.sdk.core.cache import ObjectCache
3
+ from hpcflow.sdk.core.test_utils import make_workflow
4
+
5
+
6
+ def test_object_cache_dependencies_simple(tmp_path: Path):
7
+ wk = make_workflow(
8
+ schemas_spec=[
9
+ ({"p1": None}, ("p2",), "t1"),
10
+ ({"p2": None}, ("p3",), "t2"),
11
+ ({"p3": None}, ("p4",), "t3"),
12
+ ({"p4": None}, ("p5",), "t4"),
13
+ ],
14
+ path=tmp_path,
15
+ local_inputs={0: ("p1",)},
16
+ overwrite=True,
17
+ )
18
+ obj_cache = ObjectCache.build(wk, dependencies=True)
19
+ assert obj_cache.run_dependencies == {0: set(), 1: {0}, 2: {1}, 3: {2}}
20
+ assert obj_cache.run_dependents == {0: {1}, 1: {2}, 2: {3}, 3: set()}
21
+ assert obj_cache.iter_run_dependencies == {0: set(), 1: {0}, 2: {1}, 3: {2}}
22
+ assert obj_cache.iter_iter_dependencies == {
23
+ 0: set(),
24
+ 1: {0},
25
+ 2: {1},
26
+ 3: {2},
27
+ }
28
+ assert obj_cache.elem_iter_dependencies == {
29
+ 0: set(),
30
+ 1: {0},
31
+ 2: {1},
32
+ 3: {2},
33
+ }
34
+ assert obj_cache.elem_elem_dependencies == {
35
+ 0: set(),
36
+ 1: {0},
37
+ 2: {1},
38
+ 3: {2},
39
+ }
40
+ assert obj_cache.elem_elem_dependents == {0: {1}, 1: {2}, 2: {3}, 3: set()}
41
+ assert obj_cache.elem_elem_dependents_rec == {
42
+ 0: {1, 2, 3},
43
+ 1: {2, 3},
44
+ 2: {3},
45
+ 3: set(),
46
+ }
@@ -1,12 +1,145 @@
1
1
  import pytest
2
2
 
3
3
  from click.testing import CliRunner
4
+ import click.exceptions
4
5
 
5
6
  from hpcflow import __version__
6
7
  from hpcflow.app import app as hf
8
+ from hpcflow.sdk.cli import ErrorPropagatingClickContext
9
+ from hpcflow.sdk.cli_common import BoolOrString
7
10
 
8
11
 
9
- def test_version():
12
+ def test_version() -> None:
10
13
  runner = CliRunner()
11
14
  result = runner.invoke(hf.cli, args="--version")
12
15
  assert result.output.strip() == f"hpcFlow, version {__version__}"
16
+
17
+
18
+ def test_BoolOrString_convert():
19
+ param_type = BoolOrString(["a"])
20
+ assert param_type.convert(True, None, None) == True
21
+ assert param_type.convert(False, None, None) == False
22
+ assert param_type.convert("yes", None, None) == True
23
+ assert param_type.convert("no", None, None) == False
24
+ assert param_type.convert("on", None, None) == True
25
+ assert param_type.convert("off", None, None) == False
26
+ assert param_type.convert("a", None, None) == "a"
27
+ with pytest.raises(click.exceptions.BadParameter):
28
+ param_type.convert("b", None, None)
29
+
30
+
31
+ def test_error_propagated_with_custom_context_class():
32
+ class MyException(ValueError):
33
+ pass
34
+
35
+ class MyContextManager:
36
+
37
+ # set to True when MyException is raised within this context manager
38
+ raised = False
39
+
40
+ def __enter__(self):
41
+ return self
42
+
43
+ def __exit__(self, exc_type, exc_val, exc_tb):
44
+ if exc_type == MyException:
45
+ self.__class__.raised = True
46
+
47
+ @click.group()
48
+ @click.pass_context
49
+ def cli(ctx):
50
+ ctx.with_resource(MyContextManager())
51
+
52
+ cli.context_class = ErrorPropagatingClickContext # use custom click Context
53
+
54
+ @cli.command()
55
+ def my_command():
56
+ raise MyException()
57
+
58
+ runner = CliRunner()
59
+ runner.invoke(cli, args="my-command")
60
+
61
+ assert MyContextManager.raised
62
+
63
+
64
+ def test_error_not_propagated_without_custom_context_class():
65
+ class MyException(ValueError):
66
+ pass
67
+
68
+ class MyContextManager:
69
+
70
+ # set to True when MyException is raised within this context manager
71
+ raised = False
72
+
73
+ def __enter__(self):
74
+ return self
75
+
76
+ def __exit__(self, exc_type, exc_val, exc_tb):
77
+ if exc_type == MyException:
78
+ self.__class__.raised = True
79
+
80
+ @click.group()
81
+ @click.pass_context
82
+ def cli(ctx):
83
+ ctx.with_resource(MyContextManager())
84
+
85
+ @cli.command()
86
+ def my_command():
87
+ raise MyException()
88
+
89
+ runner = CliRunner()
90
+ runner.invoke(cli, args="my-command")
91
+
92
+ assert not MyContextManager.raised
93
+
94
+
95
+ def test_std_stream_file_created(tmp_path):
96
+ """Test exception is intercepted and printed to the specified --std-stream file."""
97
+ error_file = tmp_path / "std_stream.txt"
98
+ runner = CliRunner()
99
+ result = runner.invoke(
100
+ hf.cli, args=f'--std-stream "{str(error_file)}" internal noop --raise'
101
+ )
102
+ assert error_file.is_file()
103
+ std_stream_contents = error_file.read_text()
104
+ assert "ValueError: internal noop raised!" in std_stream_contents
105
+ assert result.exit_code == 1
106
+ assert result.exc_info[0] == SystemExit
107
+
108
+
109
+ def test_std_stream_file_not_created(null_config, tmp_path):
110
+ """Test std stream file is not created when no ouput/errors/exceptions"""
111
+ error_file = tmp_path / "std_stream.txt"
112
+ runner = CliRunner()
113
+ result = runner.invoke(hf.cli, args=f'--std-stream "{str(error_file)}" internal noop')
114
+ assert not error_file.is_file()
115
+ assert result.exit_code == 0
116
+
117
+
118
+ def test_cli_exception():
119
+ """Test exception is passed to click"""
120
+ runner = CliRunner()
121
+ result = runner.invoke(hf.cli, args="internal noop --raise")
122
+ assert result.exit_code == 1
123
+ assert result.exc_info[0] == ValueError
124
+
125
+
126
+ def test_cli_click_exit_code_zero(tmp_path):
127
+ """Test Click's `Exit` exception is ignored by the `redirect_std_to_file` context manager when the exit code is zero."""
128
+ error_file = tmp_path / "std_stream.txt"
129
+ runner = CliRunner()
130
+ result = runner.invoke(
131
+ hf.cli, args=f'--std-stream "{str(error_file)}" internal noop --click-exit-code 0'
132
+ )
133
+ assert result.exit_code == 0
134
+ assert not error_file.is_file()
135
+
136
+
137
+ def test_cli_click_exit_code_non_zero(tmp_path):
138
+ """Test Click's `Exit` exception is not ignored by the `redirect_std_to_file` context manager when the exit code is non-zero."""
139
+ error_file = tmp_path / "std_stream.txt"
140
+ runner = CliRunner()
141
+ result = runner.invoke(
142
+ hf.cli, args=f'--std-stream "{str(error_file)}" internal noop --click-exit-code 2'
143
+ )
144
+ assert result.exit_code == 2
145
+ assert error_file.is_file()