hpcflow-new2 0.2.0a190__py3-none-any.whl → 0.2.0a200__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 (132) hide show
  1. hpcflow/__pyinstaller/hook-hpcflow.py +1 -0
  2. hpcflow/_version.py +1 -1
  3. hpcflow/data/scripts/bad_script.py +2 -0
  4. hpcflow/data/scripts/do_nothing.py +2 -0
  5. hpcflow/data/scripts/env_specifier_test/input_file_generator_pass_env_spec.py +4 -0
  6. hpcflow/data/scripts/env_specifier_test/main_script_test_pass_env_spec.py +8 -0
  7. hpcflow/data/scripts/env_specifier_test/output_file_parser_pass_env_spec.py +4 -0
  8. hpcflow/data/scripts/env_specifier_test/v1/input_file_generator_basic.py +4 -0
  9. hpcflow/data/scripts/env_specifier_test/v1/main_script_test_direct_in_direct_out.py +7 -0
  10. hpcflow/data/scripts/env_specifier_test/v1/output_file_parser_basic.py +4 -0
  11. hpcflow/data/scripts/env_specifier_test/v2/main_script_test_direct_in_direct_out.py +7 -0
  12. hpcflow/data/scripts/input_file_generator_basic.py +3 -0
  13. hpcflow/data/scripts/input_file_generator_basic_FAIL.py +3 -0
  14. hpcflow/data/scripts/input_file_generator_test_stdout_stderr.py +8 -0
  15. hpcflow/data/scripts/main_script_test_direct_in.py +3 -0
  16. hpcflow/data/scripts/main_script_test_direct_in_direct_out_2.py +6 -0
  17. hpcflow/data/scripts/main_script_test_direct_in_direct_out_2_fail_allowed.py +6 -0
  18. hpcflow/data/scripts/main_script_test_direct_in_direct_out_2_fail_allowed_group.py +7 -0
  19. hpcflow/data/scripts/main_script_test_direct_in_direct_out_3.py +6 -0
  20. hpcflow/data/scripts/main_script_test_direct_in_group_direct_out_3.py +6 -0
  21. hpcflow/data/scripts/main_script_test_direct_in_group_one_fail_direct_out_3.py +6 -0
  22. hpcflow/data/scripts/main_script_test_hdf5_in_obj_2.py +12 -0
  23. hpcflow/data/scripts/main_script_test_json_out_FAIL.py +3 -0
  24. hpcflow/data/scripts/main_script_test_shell_env_vars.py +12 -0
  25. hpcflow/data/scripts/main_script_test_std_out_std_err.py +6 -0
  26. hpcflow/data/scripts/output_file_parser_basic.py +3 -0
  27. hpcflow/data/scripts/output_file_parser_basic_FAIL.py +7 -0
  28. hpcflow/data/scripts/output_file_parser_test_stdout_stderr.py +8 -0
  29. hpcflow/data/scripts/script_exit_test.py +5 -0
  30. hpcflow/data/template_components/environments.yaml +1 -1
  31. hpcflow/sdk/__init__.py +5 -0
  32. hpcflow/sdk/app.py +166 -92
  33. hpcflow/sdk/cli.py +263 -84
  34. hpcflow/sdk/cli_common.py +99 -5
  35. hpcflow/sdk/config/callbacks.py +38 -1
  36. hpcflow/sdk/config/config.py +102 -13
  37. hpcflow/sdk/config/errors.py +19 -5
  38. hpcflow/sdk/config/types.py +3 -0
  39. hpcflow/sdk/core/__init__.py +25 -1
  40. hpcflow/sdk/core/actions.py +914 -262
  41. hpcflow/sdk/core/cache.py +76 -34
  42. hpcflow/sdk/core/command_files.py +14 -128
  43. hpcflow/sdk/core/commands.py +35 -6
  44. hpcflow/sdk/core/element.py +122 -50
  45. hpcflow/sdk/core/errors.py +58 -2
  46. hpcflow/sdk/core/execute.py +207 -0
  47. hpcflow/sdk/core/loop.py +408 -50
  48. hpcflow/sdk/core/loop_cache.py +4 -4
  49. hpcflow/sdk/core/parameters.py +382 -37
  50. hpcflow/sdk/core/run_dir_files.py +13 -40
  51. hpcflow/sdk/core/skip_reason.py +7 -0
  52. hpcflow/sdk/core/task.py +119 -30
  53. hpcflow/sdk/core/task_schema.py +68 -0
  54. hpcflow/sdk/core/test_utils.py +66 -27
  55. hpcflow/sdk/core/types.py +54 -1
  56. hpcflow/sdk/core/utils.py +136 -19
  57. hpcflow/sdk/core/workflow.py +1587 -356
  58. hpcflow/sdk/data/workflow_spec_schema.yaml +2 -0
  59. hpcflow/sdk/demo/cli.py +7 -0
  60. hpcflow/sdk/helper/cli.py +1 -0
  61. hpcflow/sdk/log.py +42 -15
  62. hpcflow/sdk/persistence/base.py +405 -53
  63. hpcflow/sdk/persistence/json.py +177 -52
  64. hpcflow/sdk/persistence/pending.py +237 -69
  65. hpcflow/sdk/persistence/store_resource.py +3 -2
  66. hpcflow/sdk/persistence/types.py +15 -4
  67. hpcflow/sdk/persistence/zarr.py +928 -81
  68. hpcflow/sdk/submission/jobscript.py +1408 -489
  69. hpcflow/sdk/submission/schedulers/__init__.py +40 -5
  70. hpcflow/sdk/submission/schedulers/direct.py +33 -19
  71. hpcflow/sdk/submission/schedulers/sge.py +51 -16
  72. hpcflow/sdk/submission/schedulers/slurm.py +44 -16
  73. hpcflow/sdk/submission/schedulers/utils.py +7 -2
  74. hpcflow/sdk/submission/shells/base.py +68 -20
  75. hpcflow/sdk/submission/shells/bash.py +222 -129
  76. hpcflow/sdk/submission/shells/powershell.py +200 -150
  77. hpcflow/sdk/submission/submission.py +852 -119
  78. hpcflow/sdk/submission/types.py +18 -21
  79. hpcflow/sdk/typing.py +24 -5
  80. hpcflow/sdk/utils/arrays.py +71 -0
  81. hpcflow/sdk/utils/deferred_file.py +55 -0
  82. hpcflow/sdk/utils/hashing.py +16 -0
  83. hpcflow/sdk/utils/patches.py +12 -0
  84. hpcflow/sdk/utils/strings.py +33 -0
  85. hpcflow/tests/api/test_api.py +32 -0
  86. hpcflow/tests/conftest.py +19 -0
  87. hpcflow/tests/data/benchmark_script_runner.yaml +26 -0
  88. hpcflow/tests/data/multi_path_sequences.yaml +29 -0
  89. hpcflow/tests/data/workflow_test_run_abort.yaml +34 -35
  90. hpcflow/tests/schedulers/sge/test_sge_submission.py +36 -0
  91. hpcflow/tests/scripts/test_input_file_generators.py +282 -0
  92. hpcflow/tests/scripts/test_main_scripts.py +821 -70
  93. hpcflow/tests/scripts/test_non_snippet_script.py +46 -0
  94. hpcflow/tests/scripts/test_ouput_file_parsers.py +353 -0
  95. hpcflow/tests/shells/wsl/test_wsl_submission.py +6 -0
  96. hpcflow/tests/unit/test_action.py +176 -0
  97. hpcflow/tests/unit/test_app.py +20 -0
  98. hpcflow/tests/unit/test_cache.py +46 -0
  99. hpcflow/tests/unit/test_cli.py +133 -0
  100. hpcflow/tests/unit/test_config.py +122 -1
  101. hpcflow/tests/unit/test_element_iteration.py +47 -0
  102. hpcflow/tests/unit/test_jobscript_unit.py +757 -0
  103. hpcflow/tests/unit/test_loop.py +1332 -27
  104. hpcflow/tests/unit/test_meta_task.py +325 -0
  105. hpcflow/tests/unit/test_multi_path_sequences.py +229 -0
  106. hpcflow/tests/unit/test_parameter.py +13 -0
  107. hpcflow/tests/unit/test_persistence.py +190 -8
  108. hpcflow/tests/unit/test_run.py +109 -3
  109. hpcflow/tests/unit/test_run_directories.py +29 -0
  110. hpcflow/tests/unit/test_shell.py +20 -0
  111. hpcflow/tests/unit/test_submission.py +5 -76
  112. hpcflow/tests/unit/test_workflow_template.py +31 -0
  113. hpcflow/tests/unit/utils/test_arrays.py +40 -0
  114. hpcflow/tests/unit/utils/test_deferred_file_writer.py +34 -0
  115. hpcflow/tests/unit/utils/test_hashing.py +65 -0
  116. hpcflow/tests/unit/utils/test_patches.py +5 -0
  117. hpcflow/tests/unit/utils/test_redirect_std.py +50 -0
  118. hpcflow/tests/workflows/__init__.py +0 -0
  119. hpcflow/tests/workflows/test_directory_structure.py +31 -0
  120. hpcflow/tests/workflows/test_jobscript.py +332 -0
  121. hpcflow/tests/workflows/test_run_status.py +198 -0
  122. hpcflow/tests/workflows/test_skip_downstream.py +696 -0
  123. hpcflow/tests/workflows/test_submission.py +140 -0
  124. hpcflow/tests/workflows/test_workflows.py +142 -2
  125. hpcflow/tests/workflows/test_zip.py +18 -0
  126. hpcflow/viz_demo.ipynb +6587 -3
  127. {hpcflow_new2-0.2.0a190.dist-info → hpcflow_new2-0.2.0a200.dist-info}/METADATA +7 -4
  128. hpcflow_new2-0.2.0a200.dist-info/RECORD +222 -0
  129. hpcflow_new2-0.2.0a190.dist-info/RECORD +0 -165
  130. {hpcflow_new2-0.2.0a190.dist-info → hpcflow_new2-0.2.0a200.dist-info}/LICENSE +0 -0
  131. {hpcflow_new2-0.2.0a190.dist-info → hpcflow_new2-0.2.0a200.dist-info}/WHEEL +0 -0
  132. {hpcflow_new2-0.2.0a190.dist-info → hpcflow_new2-0.2.0a200.dist-info}/entry_points.txt +0 -0
hpcflow/sdk/core/types.py CHANGED
@@ -1,6 +1,7 @@
1
1
  """
2
2
  Types to support the core SDK.
3
3
  """
4
+
4
5
  from __future__ import annotations
5
6
  from typing import Any, Literal, Protocol, TYPE_CHECKING
6
7
  from typing_extensions import NotRequired, TypeAlias, TypedDict
@@ -205,6 +206,9 @@ class SchemaInputKwargs(TypedDict):
205
206
  multiple: bool
206
207
  #: The labels.
207
208
  labels: dict[str, LabelInfo] | None
209
+ #: The number or proportional of permitted unset parameter data found when reolving
210
+ #: this input from upstream outputs.
211
+ allow_failed_dependencies: int | float | bool | None
208
212
 
209
213
 
210
214
  class RuleArgs(TypedDict):
@@ -333,11 +337,51 @@ class WorkflowTemplateTaskData(TypedDict):
333
337
  #: The schema, if known.
334
338
  schema: NotRequired[Any | list[Any]]
335
339
  #: The element sets, if known.
336
- element_sets: NotRequired[list["WorkflowTemplateTaskData"]]
340
+ element_sets: NotRequired[list[WorkflowTemplateElementSetData]]
337
341
  #: The output labels, if known.
338
342
  output_labels: NotRequired[list[str]]
339
343
 
340
344
 
345
+ class WorkflowTemplateElementSetData(TypedDict):
346
+ """
347
+ Descriptor for element set data within a workflow template parametrisation.
348
+ """
349
+
350
+ #: Inputs to the set of elements.
351
+ inputs: NotRequired[list[dict[str, Any]]]
352
+ #: Input files to the set of elements.
353
+ input_files: NotRequired[list[dict[str, Any]]]
354
+ #: Description of how to repeat the set of elements.
355
+ repeats: NotRequired[int | list[RepeatsDescriptor]]
356
+ #: Groupings in the set of elements.
357
+ groups: NotRequired[list[dict[str, Any]]]
358
+ #: Resources to use for the set of elements.
359
+ resources: NotRequired[dict[str, Any]]
360
+ #: Input value sequences to parameterise over.
361
+ sequences: NotRequired[list[dict[str, Any]]]
362
+ #: Input value multi-path sequences to parameterise over.
363
+ multi_path_sequences: NotRequired[list[dict[str, Any]]]
364
+ #: Input source descriptors.
365
+ input_sources: NotRequired[dict[str, list]]
366
+ #: How to handle nesting of iterations.
367
+ nesting_order: NotRequired[dict[str, float]]
368
+ #: Which environment preset to use.
369
+ env_preset: NotRequired[str]
370
+ #: Environment descriptors to use.
371
+ environments: NotRequired[dict[str, dict[str, Any]]]
372
+ #: List of global element iteration indices from which inputs for
373
+ #: the new elements associated with this element set may be sourced.
374
+ #: If ``None``, all iterations are valid.
375
+ sourceable_elem_iters: NotRequired[list[int]]
376
+ #: Whether to allow sources to come from distinct element sub-sets.
377
+ allow_non_coincident_task_sources: NotRequired[bool]
378
+ #: Whether this initialisation is the first for this data (i.e. not a
379
+ #: reconstruction from persistent workflow data), in which case, we merge
380
+ #: ``environments`` into ``resources`` using the "any" scope, and merge any multi-
381
+ #: path sequences into the sequences list.
382
+ is_creation: NotRequired[bool]
383
+
384
+
341
385
  class Pending(TypedDict):
342
386
  """
343
387
  Pending update information. Internal use only.
@@ -385,3 +429,12 @@ class ResourcePersistingWorkflow(Protocol):
385
429
  """
386
430
  Check if all the parameters exist.
387
431
  """
432
+
433
+
434
+ BlockActionKey: TypeAlias = "tuple[int | str, int | str, int | str]"
435
+ """
436
+ The type of indices that locate an action within a submission. The indices represent,
437
+ respectively, the jobscript index, the jobscript-block index, and the block-action index.
438
+ Usually, these are integers, but in the case of strings, they will correspond to shell
439
+ environment variables.
440
+ """
hpcflow/sdk/core/utils.py CHANGED
@@ -4,8 +4,12 @@ Miscellaneous utilities.
4
4
 
5
5
  from __future__ import annotations
6
6
  from collections import Counter
7
+ from asyncio import events
8
+ import contextvars
9
+ import contextlib
7
10
  import copy
8
11
  import enum
12
+ import functools
9
13
  import hashlib
10
14
  from itertools import accumulate, islice
11
15
  from importlib import resources
@@ -20,7 +24,8 @@ import string
20
24
  import subprocess
21
25
  from datetime import datetime, timedelta, timezone
22
26
  import sys
23
- from typing import cast, overload, TypeVar, TYPE_CHECKING
27
+ import traceback
28
+ from typing import Literal, cast, overload, TypeVar, TYPE_CHECKING
24
29
  import fsspec # type: ignore
25
30
  import numpy as np
26
31
 
@@ -33,12 +38,13 @@ from hpcflow.sdk.core.errors import (
33
38
  MissingVariableSubstitutionError,
34
39
  )
35
40
  from hpcflow.sdk.log import TimeIt
41
+ from hpcflow.sdk.utils.deferred_file import DeferredFileWriter
36
42
 
37
43
  if TYPE_CHECKING:
38
44
  from collections.abc import Callable, Hashable, Iterable, Mapping, Sequence
39
45
  from contextlib import AbstractContextManager
40
46
  from types import ModuleType
41
- from typing import Any, IO
47
+ from typing import Any, IO, Iterator
42
48
  from typing_extensions import TypeAlias
43
49
  from numpy.typing import NDArray
44
50
  from ..typing import PathLike
@@ -124,7 +130,9 @@ def check_valid_py_identifier(name: str) -> str:
124
130
 
125
131
 
126
132
  @overload
127
- def group_by_dict_key_values(lst: list[dict[T, T2]], key: T) -> list[list[dict[T, T2]]]:
133
+ def group_by_dict_key_values( # type: ignore[overload-overlap]
134
+ lst: list[dict[T, T2]], key: T
135
+ ) -> list[list[dict[T, T2]]]:
128
136
  ...
129
137
 
130
138
 
@@ -326,7 +334,7 @@ def get_relative_path(path1: Sequence[T], path2: Sequence[T]) -> Sequence[T]:
326
334
 
327
335
 
328
336
  def search_dir_files_by_regex(
329
- pattern: str | re.Pattern[str], directory: str = "."
337
+ pattern: str | re.Pattern[str], directory: str | os.PathLike = "."
330
338
  ) -> list[str]:
331
339
  """Search recursively for files in a directory by a regex pattern and return matching
332
340
  file paths, relative to the given directory."""
@@ -406,20 +414,44 @@ def substitute_string_vars(string: str, variables: dict[str, str]):
406
414
 
407
415
  @TimeIt.decorator
408
416
  def read_YAML_str(
409
- yaml_str: str, typ="safe", variables: dict[str, str] | None = None
417
+ yaml_str: str, typ="safe", variables: dict[str, str] | Literal[False] | None = None
410
418
  ) -> Any:
411
- """Load a YAML string. This will produce basic objects."""
412
- if variables is not None and "<<var:" in yaml_str:
413
- yaml_str = substitute_string_vars(yaml_str, variables=variables)
419
+ """Load a YAML string. This will produce basic objects.
420
+
421
+ Parameters
422
+ ----------
423
+ yaml_str:
424
+ The YAML string to parse.
425
+ typ:
426
+ Load type passed to the YAML library.
427
+ variables:
428
+ String variables to substitute in `yaml_str`. Substitutions will be attempted if
429
+ the file looks to contain variable references (like "<<var:name>>"). If set to
430
+ `False`, no substitutions will occur.
431
+ """
432
+ if variables is not False and "<<var:" in yaml_str:
433
+ yaml_str = substitute_string_vars(yaml_str, variables=variables or {})
414
434
  yaml = YAML(typ=typ)
415
435
  return yaml.load(yaml_str)
416
436
 
417
437
 
418
438
  @TimeIt.decorator
419
439
  def read_YAML_file(
420
- path: PathLike, typ="safe", variables: dict[str, str] | None = None
440
+ path: PathLike, typ="safe", variables: dict[str, str] | Literal[False] | None = None
421
441
  ) -> Any:
422
- """Load a YAML file. This will produce basic objects."""
442
+ """Load a YAML file. This will produce basic objects.
443
+
444
+ Parameters
445
+ ----------
446
+ path:
447
+ Path to the YAML file to parse.
448
+ typ:
449
+ Load type passed to the YAML library.
450
+ variables:
451
+ String variables to substitute in the file given by `path`. Substitutions will be
452
+ attempted if the file looks to contain variable references (like "<<var:name>>").
453
+ If set to `False`, no substitutions will occur.
454
+ """
423
455
  with fsspec.open(path, "rt") as f:
424
456
  yaml_str: str = f.read()
425
457
  return read_YAML_str(yaml_str, typ=typ, variables=variables)
@@ -432,15 +464,37 @@ def write_YAML_file(obj, path: str | Path, typ: str = "safe") -> None:
432
464
  yaml.dump(obj, fp)
433
465
 
434
466
 
435
- def read_JSON_string(json_str: str, variables: dict[str, str] | None = None) -> Any:
436
- """Load a JSON string. This will produce basic objects."""
437
- if variables is not None and "<<var:" in json_str:
438
- json_str = substitute_string_vars(json_str, variables=variables)
467
+ def read_JSON_string(
468
+ json_str: str, variables: dict[str, str] | Literal[False] | None = None
469
+ ) -> Any:
470
+ """Load a JSON string. This will produce basic objects.
471
+
472
+ Parameters
473
+ ----------
474
+ json_str:
475
+ The JSON string to parse.
476
+ variables:
477
+ String variables to substitute in `json_str`. Substitutions will be attempted if
478
+ the file looks to contain variable references (like "<<var:name>>"). If set to
479
+ `False`, no substitutions will occur.
480
+ """
481
+ if variables is not False and "<<var:" in json_str:
482
+ json_str = substitute_string_vars(json_str, variables=variables or {})
439
483
  return json.loads(json_str)
440
484
 
441
485
 
442
- def read_JSON_file(path, variables: dict[str, str] | None = None) -> Any:
443
- """Load a JSON file. This will produce basic objects."""
486
+ def read_JSON_file(path, variables: dict[str, str] | Literal[False] | None = None) -> Any:
487
+ """Load a JSON file. This will produce basic objects.
488
+
489
+ Parameters
490
+ ----------
491
+ path:
492
+ Path to the JSON file to parse.
493
+ variables:
494
+ String variables to substitute in the file given by `path`. Substitutions will be
495
+ attempted if the file looks to contain variable references (like "<<var:name>>").
496
+ If set to `False`, no substitutions will occur.
497
+ """
444
498
  with fsspec.open(path, "rt") as f:
445
499
  json_str: str = f.read()
446
500
  return read_JSON_string(json_str, variables=variables)
@@ -771,7 +825,12 @@ class JSONLikeDirSnapShot(DirectorySnapshot):
771
825
  See :py:meth:`to_json_like`.
772
826
  """
773
827
 
774
- def __init__(self, root_path: str | None = None, data: dict[str, list] | None = None):
828
+ def __init__(
829
+ self,
830
+ root_path: str | None = None,
831
+ data: dict[str, list] | None = None,
832
+ use_strings: bool = False,
833
+ ):
775
834
  """
776
835
  Create an empty snapshot or load from JSON-like data.
777
836
  """
@@ -786,6 +845,7 @@ class JSONLikeDirSnapShot(DirectorySnapshot):
786
845
  for name, item in data.items():
787
846
  # add root path
788
847
  full_name = str(PurePath(root_path) / PurePath(name))
848
+ item = [int(i) for i in item] if use_strings else item
789
849
  stat_dat, inode_key = item[:-2], item[-2:]
790
850
  self._stat_info[full_name] = os.stat_result(stat_dat)
791
851
  self._inode_to_path[tuple(inode_key)] = full_name
@@ -794,7 +854,7 @@ class JSONLikeDirSnapShot(DirectorySnapshot):
794
854
  """Take the snapshot."""
795
855
  super().__init__(*args, **kwargs)
796
856
 
797
- def to_json_like(self) -> dict[str, Any]:
857
+ def to_json_like(self, use_strings: bool = False) -> dict[str, Any]:
798
858
  """Export to a dict that is JSON-compatible and can be later reloaded.
799
859
 
800
860
  The last two integers in `data` for each path are the keys in
@@ -807,13 +867,16 @@ class JSONLikeDirSnapShot(DirectorySnapshot):
807
867
  # store efficiently:
808
868
  inode_invert = {v: k for k, v in self._inode_to_path.items()}
809
869
  data: dict[str, list] = {
810
- str(PurePath(k).relative_to(root_path)): [*v, *inode_invert[k]]
870
+ str(PurePath(k).relative_to(root_path)): [
871
+ str(i) if use_strings else i for i in [*v, *inode_invert[k]]
872
+ ]
811
873
  for k, v in self._stat_info.items()
812
874
  }
813
875
 
814
876
  return {
815
877
  "root_path": root_path,
816
878
  "data": data,
879
+ "use_strings": use_strings,
817
880
  }
818
881
 
819
882
 
@@ -1083,3 +1146,57 @@ def get_file_context(
1083
1146
  except AttributeError:
1084
1147
  # < python 3.9
1085
1148
  return resources.path(package, src or "")
1149
+
1150
+
1151
+ @contextlib.contextmanager
1152
+ def redirect_std_to_file(
1153
+ file,
1154
+ mode: Literal["w", "a"] = "a",
1155
+ ignore: Callable[[BaseException], Literal[True] | int] | None = None,
1156
+ ) -> Iterator[None]:
1157
+ """Temporarily redirect both stdout and stderr to a file, and if an exception is
1158
+ raised, catch it, print the traceback to that file, and exit.
1159
+
1160
+ File creation is deferred until an actual write is required.
1161
+
1162
+ Parameters
1163
+ ----------
1164
+ ignore
1165
+ Callable to test if a given exception should be ignored. If an exception is
1166
+ not ignored, its traceback will be printed to `file` and the program will
1167
+ exit with exit code 1. The callable should accept one parameter, the
1168
+ exception, and should return True if that exception should be ignored, or
1169
+ an integer representing the exit code to exit the program with if that
1170
+ exception should not be ignored. By default, no exceptions are ignored.
1171
+
1172
+ """
1173
+ ignore = ignore or (lambda _: 1)
1174
+ with DeferredFileWriter(file, mode=mode) as fp:
1175
+ with contextlib.redirect_stdout(fp):
1176
+ with contextlib.redirect_stderr(fp):
1177
+ try:
1178
+ yield
1179
+ except BaseException as exc:
1180
+ ignore_ret = ignore(exc)
1181
+ if ignore_ret is not True:
1182
+ traceback.print_exc()
1183
+ sys.exit(ignore_ret)
1184
+
1185
+
1186
+ async def to_thread(func, /, *args, **kwargs):
1187
+ """Copied from https://github.com/python/cpython/blob/4b4227b907a262446b9d276c274feda2590a4e6e/Lib/asyncio/threads.py
1188
+ to support Python 3.8, which does not have `asyncio.to_thread`.
1189
+
1190
+ Asynchronously run function *func* in a separate thread.
1191
+
1192
+ Any *args and **kwargs supplied for this function are directly passed
1193
+ to *func*. Also, the current :class:`contextvars.Context` is propagated,
1194
+ allowing context variables from the main thread to be accessed in the
1195
+ separate thread.
1196
+
1197
+ Return a coroutine that can be awaited to get the eventual result of *func*.
1198
+ """
1199
+ loop = events.get_running_loop()
1200
+ ctx = contextvars.copy_context()
1201
+ func_call = functools.partial(ctx.run, func, *args, **kwargs)
1202
+ return await loop.run_in_executor(None, func_call)