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.
- hpcflow/__pyinstaller/hook-hpcflow.py +1 -0
- hpcflow/_version.py +1 -1
- hpcflow/data/scripts/bad_script.py +2 -0
- hpcflow/data/scripts/do_nothing.py +2 -0
- hpcflow/data/scripts/env_specifier_test/input_file_generator_pass_env_spec.py +4 -0
- hpcflow/data/scripts/env_specifier_test/main_script_test_pass_env_spec.py +8 -0
- hpcflow/data/scripts/env_specifier_test/output_file_parser_pass_env_spec.py +4 -0
- hpcflow/data/scripts/env_specifier_test/v1/input_file_generator_basic.py +4 -0
- hpcflow/data/scripts/env_specifier_test/v1/main_script_test_direct_in_direct_out.py +7 -0
- hpcflow/data/scripts/env_specifier_test/v1/output_file_parser_basic.py +4 -0
- hpcflow/data/scripts/env_specifier_test/v2/main_script_test_direct_in_direct_out.py +7 -0
- hpcflow/data/scripts/input_file_generator_basic.py +3 -0
- hpcflow/data/scripts/input_file_generator_basic_FAIL.py +3 -0
- hpcflow/data/scripts/input_file_generator_test_stdout_stderr.py +8 -0
- hpcflow/data/scripts/main_script_test_direct_in.py +3 -0
- hpcflow/data/scripts/main_script_test_direct_in_direct_out_2.py +6 -0
- hpcflow/data/scripts/main_script_test_direct_in_direct_out_2_fail_allowed.py +6 -0
- hpcflow/data/scripts/main_script_test_direct_in_direct_out_2_fail_allowed_group.py +7 -0
- hpcflow/data/scripts/main_script_test_direct_in_direct_out_3.py +6 -0
- hpcflow/data/scripts/main_script_test_direct_in_group_direct_out_3.py +6 -0
- hpcflow/data/scripts/main_script_test_direct_in_group_one_fail_direct_out_3.py +6 -0
- hpcflow/data/scripts/main_script_test_hdf5_in_obj_2.py +12 -0
- hpcflow/data/scripts/main_script_test_json_out_FAIL.py +3 -0
- hpcflow/data/scripts/main_script_test_shell_env_vars.py +12 -0
- hpcflow/data/scripts/main_script_test_std_out_std_err.py +6 -0
- hpcflow/data/scripts/output_file_parser_basic.py +3 -0
- hpcflow/data/scripts/output_file_parser_basic_FAIL.py +7 -0
- hpcflow/data/scripts/output_file_parser_test_stdout_stderr.py +8 -0
- hpcflow/data/scripts/script_exit_test.py +5 -0
- hpcflow/data/template_components/environments.yaml +1 -1
- hpcflow/sdk/__init__.py +5 -0
- hpcflow/sdk/app.py +166 -92
- hpcflow/sdk/cli.py +263 -84
- hpcflow/sdk/cli_common.py +99 -5
- hpcflow/sdk/config/callbacks.py +38 -1
- hpcflow/sdk/config/config.py +102 -13
- hpcflow/sdk/config/errors.py +19 -5
- hpcflow/sdk/config/types.py +3 -0
- hpcflow/sdk/core/__init__.py +25 -1
- hpcflow/sdk/core/actions.py +914 -262
- hpcflow/sdk/core/cache.py +76 -34
- hpcflow/sdk/core/command_files.py +14 -128
- hpcflow/sdk/core/commands.py +35 -6
- hpcflow/sdk/core/element.py +122 -50
- hpcflow/sdk/core/errors.py +58 -2
- hpcflow/sdk/core/execute.py +207 -0
- hpcflow/sdk/core/loop.py +408 -50
- hpcflow/sdk/core/loop_cache.py +4 -4
- hpcflow/sdk/core/parameters.py +382 -37
- hpcflow/sdk/core/run_dir_files.py +13 -40
- hpcflow/sdk/core/skip_reason.py +7 -0
- hpcflow/sdk/core/task.py +119 -30
- hpcflow/sdk/core/task_schema.py +68 -0
- hpcflow/sdk/core/test_utils.py +66 -27
- hpcflow/sdk/core/types.py +54 -1
- hpcflow/sdk/core/utils.py +136 -19
- hpcflow/sdk/core/workflow.py +1587 -356
- hpcflow/sdk/data/workflow_spec_schema.yaml +2 -0
- hpcflow/sdk/demo/cli.py +7 -0
- hpcflow/sdk/helper/cli.py +1 -0
- hpcflow/sdk/log.py +42 -15
- hpcflow/sdk/persistence/base.py +405 -53
- hpcflow/sdk/persistence/json.py +177 -52
- hpcflow/sdk/persistence/pending.py +237 -69
- hpcflow/sdk/persistence/store_resource.py +3 -2
- hpcflow/sdk/persistence/types.py +15 -4
- hpcflow/sdk/persistence/zarr.py +928 -81
- hpcflow/sdk/submission/jobscript.py +1408 -489
- hpcflow/sdk/submission/schedulers/__init__.py +40 -5
- hpcflow/sdk/submission/schedulers/direct.py +33 -19
- hpcflow/sdk/submission/schedulers/sge.py +51 -16
- hpcflow/sdk/submission/schedulers/slurm.py +44 -16
- hpcflow/sdk/submission/schedulers/utils.py +7 -2
- hpcflow/sdk/submission/shells/base.py +68 -20
- hpcflow/sdk/submission/shells/bash.py +222 -129
- hpcflow/sdk/submission/shells/powershell.py +200 -150
- hpcflow/sdk/submission/submission.py +852 -119
- hpcflow/sdk/submission/types.py +18 -21
- hpcflow/sdk/typing.py +24 -5
- hpcflow/sdk/utils/arrays.py +71 -0
- hpcflow/sdk/utils/deferred_file.py +55 -0
- hpcflow/sdk/utils/hashing.py +16 -0
- hpcflow/sdk/utils/patches.py +12 -0
- hpcflow/sdk/utils/strings.py +33 -0
- hpcflow/tests/api/test_api.py +32 -0
- hpcflow/tests/conftest.py +19 -0
- hpcflow/tests/data/benchmark_script_runner.yaml +26 -0
- hpcflow/tests/data/multi_path_sequences.yaml +29 -0
- hpcflow/tests/data/workflow_test_run_abort.yaml +34 -35
- hpcflow/tests/schedulers/sge/test_sge_submission.py +36 -0
- hpcflow/tests/scripts/test_input_file_generators.py +282 -0
- hpcflow/tests/scripts/test_main_scripts.py +821 -70
- hpcflow/tests/scripts/test_non_snippet_script.py +46 -0
- hpcflow/tests/scripts/test_ouput_file_parsers.py +353 -0
- hpcflow/tests/shells/wsl/test_wsl_submission.py +6 -0
- hpcflow/tests/unit/test_action.py +176 -0
- hpcflow/tests/unit/test_app.py +20 -0
- hpcflow/tests/unit/test_cache.py +46 -0
- hpcflow/tests/unit/test_cli.py +133 -0
- hpcflow/tests/unit/test_config.py +122 -1
- hpcflow/tests/unit/test_element_iteration.py +47 -0
- hpcflow/tests/unit/test_jobscript_unit.py +757 -0
- hpcflow/tests/unit/test_loop.py +1332 -27
- hpcflow/tests/unit/test_meta_task.py +325 -0
- hpcflow/tests/unit/test_multi_path_sequences.py +229 -0
- hpcflow/tests/unit/test_parameter.py +13 -0
- hpcflow/tests/unit/test_persistence.py +190 -8
- hpcflow/tests/unit/test_run.py +109 -3
- hpcflow/tests/unit/test_run_directories.py +29 -0
- hpcflow/tests/unit/test_shell.py +20 -0
- hpcflow/tests/unit/test_submission.py +5 -76
- hpcflow/tests/unit/test_workflow_template.py +31 -0
- hpcflow/tests/unit/utils/test_arrays.py +40 -0
- hpcflow/tests/unit/utils/test_deferred_file_writer.py +34 -0
- hpcflow/tests/unit/utils/test_hashing.py +65 -0
- hpcflow/tests/unit/utils/test_patches.py +5 -0
- hpcflow/tests/unit/utils/test_redirect_std.py +50 -0
- hpcflow/tests/workflows/__init__.py +0 -0
- hpcflow/tests/workflows/test_directory_structure.py +31 -0
- hpcflow/tests/workflows/test_jobscript.py +332 -0
- hpcflow/tests/workflows/test_run_status.py +198 -0
- hpcflow/tests/workflows/test_skip_downstream.py +696 -0
- hpcflow/tests/workflows/test_submission.py +140 -0
- hpcflow/tests/workflows/test_workflows.py +142 -2
- hpcflow/tests/workflows/test_zip.py +18 -0
- hpcflow/viz_demo.ipynb +6587 -3
- {hpcflow_new2-0.2.0a190.dist-info → hpcflow_new2-0.2.0a200.dist-info}/METADATA +7 -4
- hpcflow_new2-0.2.0a200.dist-info/RECORD +222 -0
- hpcflow_new2-0.2.0a190.dist-info/RECORD +0 -165
- {hpcflow_new2-0.2.0a190.dist-info → hpcflow_new2-0.2.0a200.dist-info}/LICENSE +0 -0
- {hpcflow_new2-0.2.0a190.dist-info → hpcflow_new2-0.2.0a200.dist-info}/WHEEL +0 -0
- {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[
|
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
|
-
|
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(
|
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
|
-
|
413
|
-
|
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(
|
436
|
-
|
437
|
-
|
438
|
-
|
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__(
|
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)): [
|
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)
|