runem 0.6.0__tar.gz → 0.7.0__tar.gz
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.
- {runem-0.6.0 → runem-0.7.0}/HISTORY.md +49 -0
- {runem-0.6.0 → runem-0.7.0}/PKG-INFO +9 -8
- {runem-0.6.0 → runem-0.7.0}/pyproject.toml +8 -6
- runem-0.7.0/runem/VERSION +1 -0
- {runem-0.6.0 → runem-0.7.0}/runem/cli/initialise_options.py +0 -1
- {runem-0.6.0 → runem-0.7.0}/runem/command_line.py +4 -7
- {runem-0.6.0 → runem-0.7.0}/runem/config.py +8 -4
- {runem-0.6.0 → runem-0.7.0}/runem/config_parse.py +2 -1
- runem-0.7.0/runem/config_validate.py +47 -0
- {runem-0.6.0 → runem-0.7.0}/runem/informative_dict.py +7 -2
- {runem-0.6.0 → runem-0.7.0}/runem/job.py +1 -3
- {runem-0.6.0 → runem-0.7.0}/runem/job_execute.py +12 -9
- {runem-0.6.0 → runem-0.7.0}/runem/job_filter.py +5 -5
- {runem-0.6.0 → runem-0.7.0}/runem/job_wrapper_python.py +3 -4
- {runem-0.6.0 → runem-0.7.0}/runem/log.py +5 -2
- {runem-0.6.0 → runem-0.7.0}/runem/report.py +4 -3
- {runem-0.6.0 → runem-0.7.0}/runem/run_command.py +8 -10
- {runem-0.6.0 → runem-0.7.0}/runem/runem.py +10 -7
- runem-0.7.0/runem/schema.yml +137 -0
- runem-0.7.0/runem/types/errors.py +14 -0
- {runem-0.6.0 → runem-0.7.0}/runem/types/hooks.py +1 -1
- {runem-0.6.0 → runem-0.7.0}/runem/types/types_jobs.py +21 -24
- runem-0.7.0/runem/yaml_utils.py +19 -0
- runem-0.7.0/runem/yaml_validation.py +28 -0
- {runem-0.6.0 → runem-0.7.0}/runem.egg-info/PKG-INFO +9 -8
- {runem-0.6.0 → runem-0.7.0}/runem.egg-info/SOURCES.txt +6 -0
- {runem-0.6.0 → runem-0.7.0}/runem.egg-info/requires.txt +6 -6
- {runem-0.6.0 → runem-0.7.0}/scripts/test_hooks/py.py +63 -1
- runem-0.7.0/tests/test_config_validate.py +128 -0
- {runem-0.6.0 → runem-0.7.0}/tests/test_files.py +3 -3
- {runem-0.6.0 → runem-0.7.0}/tests/test_hook_manager.py +3 -2
- {runem-0.6.0 → runem-0.7.0}/tests/test_informative_dict.py +21 -20
- {runem-0.6.0 → runem-0.7.0}/tests/test_job_filter.py +0 -2
- {runem-0.6.0 → runem-0.7.0}/tests/test_job_runner_simple_command.py +2 -1
- {runem-0.6.0 → runem-0.7.0}/tests/test_job_wrapper.py +2 -3
- {runem-0.6.0 → runem-0.7.0}/tests/test_run_command.py +10 -11
- {runem-0.6.0 → runem-0.7.0}/tests/test_runem.py +18 -16
- runem-0.7.0/tests/test_yaml_validation.py +66 -0
- runem-0.6.0/runem/VERSION +0 -1
- runem-0.6.0/runem/types/errors.py +0 -4
- {runem-0.6.0 → runem-0.7.0}/Containerfile +0 -0
- {runem-0.6.0 → runem-0.7.0}/LICENSE +0 -0
- {runem-0.6.0 → runem-0.7.0}/MANIFEST.in +0 -0
- {runem-0.6.0 → runem-0.7.0}/README.md +0 -0
- {runem-0.6.0 → runem-0.7.0}/runem/__init__.py +0 -0
- {runem-0.6.0 → runem-0.7.0}/runem/__main__.py +0 -0
- {runem-0.6.0 → runem-0.7.0}/runem/base.py +0 -0
- {runem-0.6.0 → runem-0.7.0}/runem/blocking_print.py +0 -0
- {runem-0.6.0 → runem-0.7.0}/runem/cli.py +0 -0
- {runem-0.6.0 → runem-0.7.0}/runem/config_metadata.py +0 -0
- {runem-0.6.0 → runem-0.7.0}/runem/files.py +0 -0
- {runem-0.6.0 → runem-0.7.0}/runem/hook_manager.py +0 -0
- {runem-0.6.0 → runem-0.7.0}/runem/job_runner_simple_command.py +0 -0
- {runem-0.6.0 → runem-0.7.0}/runem/job_wrapper.py +0 -0
- {runem-0.6.0 → runem-0.7.0}/runem/py.typed +0 -0
- {runem-0.6.0 → runem-0.7.0}/runem/runem_version.py +0 -0
- {runem-0.6.0 → runem-0.7.0}/runem/types/__init__.py +0 -0
- {runem-0.6.0 → runem-0.7.0}/runem/types/common.py +0 -0
- {runem-0.6.0 → runem-0.7.0}/runem/types/filters.py +0 -0
- {runem-0.6.0 → runem-0.7.0}/runem/types/options.py +0 -0
- {runem-0.6.0 → runem-0.7.0}/runem/types/runem_config.py +0 -0
- {runem-0.6.0 → runem-0.7.0}/runem/utils.py +0 -0
- {runem-0.6.0 → runem-0.7.0}/runem.egg-info/dependency_links.txt +0 -0
- {runem-0.6.0 → runem-0.7.0}/runem.egg-info/entry_points.txt +0 -0
- {runem-0.6.0 → runem-0.7.0}/runem.egg-info/top_level.txt +0 -0
- {runem-0.6.0 → runem-0.7.0}/scripts/test_hooks/__init__.py +0 -0
- {runem-0.6.0 → runem-0.7.0}/scripts/test_hooks/json_validators.py +0 -0
- {runem-0.6.0 → runem-0.7.0}/scripts/test_hooks/py.typed +0 -0
- {runem-0.6.0 → runem-0.7.0}/scripts/test_hooks/runem_hooks.py +0 -0
- {runem-0.6.0 → runem-0.7.0}/scripts/test_hooks/yarn.py +0 -0
- {runem-0.6.0 → runem-0.7.0}/setup.cfg +0 -0
- {runem-0.6.0 → runem-0.7.0}/tests/__init__.py +0 -0
- {runem-0.6.0 → runem-0.7.0}/tests/cli/test_initialise_options.py +0 -0
- {runem-0.6.0 → runem-0.7.0}/tests/conftest.py +0 -0
- {runem-0.6.0 → runem-0.7.0}/tests/data/help_output.3.10.txt +0 -0
- {runem-0.6.0 → runem-0.7.0}/tests/data/help_output.3.11.txt +0 -0
- {runem-0.6.0 → runem-0.7.0}/tests/intentional_test_error.py +0 -0
- {runem-0.6.0 → runem-0.7.0}/tests/sanitise_reports_footer.py +0 -0
- {runem-0.6.0 → runem-0.7.0}/tests/test_base.py +0 -0
- {runem-0.6.0 → runem-0.7.0}/tests/test_blocking_print.py +0 -0
- {runem-0.6.0 → runem-0.7.0}/tests/test_cli.py +0 -0
- {runem-0.6.0 → runem-0.7.0}/tests/test_config.py +0 -0
- {runem-0.6.0 → runem-0.7.0}/tests/test_config_parse.py +0 -0
- {runem-0.6.0 → runem-0.7.0}/tests/test_job.py +0 -0
- {runem-0.6.0 → runem-0.7.0}/tests/test_job_execute.py +0 -0
- {runem-0.6.0 → runem-0.7.0}/tests/test_job_wrapper_python.py +0 -0
- {runem-0.6.0 → runem-0.7.0}/tests/test_report.py +0 -0
- {runem-0.6.0 → runem-0.7.0}/tests/test_types/__init__.py +0 -0
- {runem-0.6.0 → runem-0.7.0}/tests/test_types/test_public_api.py +0 -0
@@ -4,6 +4,55 @@ Changelog
|
|
4
4
|
|
5
5
|
(unreleased)
|
6
6
|
------------
|
7
|
+
- Merge pull request #83 from
|
8
|
+
lursight/feat/no_runem_traceback_on_job_fail. [Frank Harrison]
|
9
|
+
|
10
|
+
Feat/no runem traceback on job fail
|
11
|
+
- Feat(error-only): only shows the error not the runem error tracback.
|
12
|
+
[Frank Harrison]
|
13
|
+
|
14
|
+
The runem error traceback is irelevant if the sub-task fails. So we just
|
15
|
+
show that task's output instead of the traceback for where we handle the
|
16
|
+
error.
|
17
|
+
- Feat(error-ctx): shows the failed job label as we show the causing
|
18
|
+
error. [Frank Harrison]
|
19
|
+
- Feat(remove-failed): always remove the failed job from the list of
|
20
|
+
running jobs. [Frank Harrison]
|
21
|
+
- Merge pull request #82 from lursight/feat/schema_validation. [Frank
|
22
|
+
Harrison]
|
23
|
+
|
24
|
+
feat(validation): validates the .runem.yml file against the schema
|
25
|
+
- Feat(validation): validates the .runem.yml file against the schema.
|
26
|
+
[Frank Harrison]
|
27
|
+
- Merge pull request #81 from lursight/chore/ruff. [Frank Harrison]
|
28
|
+
|
29
|
+
Chore/ruff
|
30
|
+
- Chore(ruff): some formatting change made whilst configuring ruff.
|
31
|
+
[Frank Harrison]
|
32
|
+
- Chore(ruff): use ruff as its faster/better. [Frank Harrison]
|
33
|
+
- Merge pull request #80 from lursight/chore/update_deps. [Frank
|
34
|
+
Harrison]
|
35
|
+
|
36
|
+
Chore/update deps
|
37
|
+
- Chore(deps): updates pylint 3.1.0 -> 3.3.6. [Frank Harrison]
|
38
|
+
- Chore(deps): updates pytest 8.3.3 -> 8.3.5 and pytest-cov to latest.
|
39
|
+
[Frank Harrison]
|
40
|
+
- Merge pull request #79 from
|
41
|
+
lursight/feat/removes_dectorate_param_from_log. [Frank Harrison]
|
42
|
+
|
43
|
+
feat(log): changes the semantics of log's 'decorate' to allow overriding
|
44
|
+
- Feat(log): changes the semantics of log's 'decorate' to allow
|
45
|
+
overriding. [Frank Harrison]
|
46
|
+
|
47
|
+
Also renames the log API's param decorate -> prefix.
|
48
|
+
|
49
|
+
This better represents the intent of the param as decorate was adding a
|
50
|
+
default prefix.
|
51
|
+
|
52
|
+
|
53
|
+
0.6.0 (2025-02-03)
|
54
|
+
------------------
|
55
|
+
- Release: version 0.6.0 🚀 [Frank Harrison]
|
7
56
|
- Merge pull request #78 from lursight/feat/better_error_display. [Frank
|
8
57
|
Harrison]
|
9
58
|
|
@@ -1,6 +1,6 @@
|
|
1
|
-
Metadata-Version: 2.
|
1
|
+
Metadata-Version: 2.4
|
2
2
|
Name: runem
|
3
|
-
Version: 0.
|
3
|
+
Version: 0.7.0
|
4
4
|
Summary: Awesome runem created by lursight
|
5
5
|
Author: lursight
|
6
6
|
License: Specify your license here
|
@@ -16,31 +16,32 @@ Description-Content-Type: text/markdown
|
|
16
16
|
License-File: LICENSE
|
17
17
|
Requires-Dist: packaging>=22.0
|
18
18
|
Requires-Dist: PyYAML>=5.0.0
|
19
|
+
Requires-Dist: jsonschema>=4.22
|
19
20
|
Requires-Dist: rich>10.0.0
|
20
21
|
Requires-Dist: typing_extensions>3.0.0
|
21
22
|
Provides-Extra: tests
|
22
|
-
Requires-Dist: black==24.10.0; extra == "tests"
|
23
23
|
Requires-Dist: coverage==7.5; extra == "tests"
|
24
|
-
Requires-Dist: docformatter==1.7.5; extra == "tests"
|
25
24
|
Requires-Dist: flake8-bugbear==24.2.6; extra == "tests"
|
26
25
|
Requires-Dist: flake8==7.0.0; extra == "tests"
|
27
26
|
Requires-Dist: gitchangelog==3.0.4; extra == "tests"
|
28
|
-
Requires-Dist: isort==5.13.2; extra == "tests"
|
29
27
|
Requires-Dist: mkdocs==1.5.3; extra == "tests"
|
30
28
|
Requires-Dist: mypy==1.9.0; extra == "tests"
|
31
29
|
Requires-Dist: pydocstyle==6.3.0; extra == "tests"
|
32
|
-
Requires-Dist: pylint==3.
|
30
|
+
Requires-Dist: pylint==3.3.6; extra == "tests"
|
33
31
|
Requires-Dist: pylama==8.4.1; extra == "tests"
|
34
|
-
Requires-Dist: pytest-cov==6.
|
32
|
+
Requires-Dist: pytest-cov==6.1.1; extra == "tests"
|
35
33
|
Requires-Dist: pytest-profiling==1.7.0; extra == "tests"
|
36
34
|
Requires-Dist: pytest-xdist==3.6.1; extra == "tests"
|
37
|
-
Requires-Dist: pytest==8.3.
|
35
|
+
Requires-Dist: pytest==8.3.5; extra == "tests"
|
36
|
+
Requires-Dist: ruff==0.11.6; extra == "tests"
|
38
37
|
Requires-Dist: setuptools; extra == "tests"
|
39
38
|
Requires-Dist: termplotlib==0.3.9; extra == "tests"
|
40
39
|
Requires-Dist: tox; extra == "tests"
|
41
40
|
Requires-Dist: types-PyYAML==6.0.12.20240311; extra == "tests"
|
42
41
|
Requires-Dist: requests-mock==1.11.0; extra == "tests"
|
42
|
+
Requires-Dist: types-jsonschema; extra == "tests"
|
43
43
|
Requires-Dist: types-setuptools; extra == "tests"
|
44
|
+
Dynamic: license-file
|
44
45
|
|
45
46
|
<!-- [](https://codecov.io/gh/lursight/runem) -->
|
46
47
|
[](https://github.com/lursight/runem/actions/workflows/main.yml)
|
@@ -23,7 +23,10 @@ classifiers = [
|
|
23
23
|
dependencies = [
|
24
24
|
# `packaging` is probably not needed after moving to pyproject.toml
|
25
25
|
"packaging>=22.0",
|
26
|
+
|
27
|
+
# For yml passingg and validation
|
26
28
|
"PyYAML>=5.0.0",
|
29
|
+
"jsonschema>=4.22",
|
27
30
|
|
28
31
|
# For UI Elements
|
29
32
|
"rich>10.0.0",
|
@@ -49,26 +52,25 @@ runem = ["VERSION"]
|
|
49
52
|
[project.optional-dependencies]
|
50
53
|
tests = [
|
51
54
|
# This requirements are for development and testing only, not for production.
|
52
|
-
"black==24.10.0",
|
53
55
|
"coverage==7.5",
|
54
|
-
"docformatter==1.7.5",
|
55
56
|
"flake8-bugbear==24.2.6",
|
56
57
|
"flake8==7.0.0",
|
57
58
|
"gitchangelog==3.0.4",
|
58
|
-
"isort==5.13.2",
|
59
59
|
"mkdocs==1.5.3",
|
60
60
|
"mypy==1.9.0",
|
61
61
|
"pydocstyle==6.3.0",
|
62
|
-
"pylint==3.
|
62
|
+
"pylint==3.3.6",
|
63
63
|
"pylama==8.4.1",
|
64
|
-
"pytest-cov==6.
|
64
|
+
"pytest-cov==6.1.1",
|
65
65
|
"pytest-profiling==1.7.0",
|
66
66
|
"pytest-xdist==3.6.1",
|
67
|
-
"pytest==8.3.
|
67
|
+
"pytest==8.3.5",
|
68
|
+
"ruff==0.11.6",
|
68
69
|
"setuptools",
|
69
70
|
"termplotlib==0.3.9",
|
70
71
|
"tox",
|
71
72
|
"types-PyYAML==6.0.12.20240311",
|
72
73
|
"requests-mock==1.11.0",
|
74
|
+
"types-jsonschema",
|
73
75
|
"types-setuptools",
|
74
76
|
]
|
@@ -0,0 +1 @@
|
|
1
|
+
0.7.0
|
@@ -43,10 +43,8 @@ def _get_argparse_help_formatter() -> typing.Any:
|
|
43
43
|
|
44
44
|
if use_fixed_width:
|
45
45
|
# Use custom formatter with the width specified in the environment variable
|
46
|
-
return (
|
47
|
-
|
48
|
-
prog
|
49
|
-
)
|
46
|
+
return lambda prog: HelpFormatterFixedWidth( # pylint: disable=unnecessary-lambda
|
47
|
+
prog
|
50
48
|
)
|
51
49
|
|
52
50
|
# Use default formatter
|
@@ -294,12 +292,12 @@ def parse_args(
|
|
294
292
|
error_on_log_logic(args.verbose, args.silent)
|
295
293
|
|
296
294
|
if args.show_root_path_and_exit:
|
297
|
-
log(str(config_metadata.cfg_filepath.parent),
|
295
|
+
log(str(config_metadata.cfg_filepath.parent), prefix=False)
|
298
296
|
# cleanly exit
|
299
297
|
sys.exit(0)
|
300
298
|
|
301
299
|
if args.show_version_and_exit:
|
302
|
-
log(str(get_runem_version()),
|
300
|
+
log(str(get_runem_version()), prefix=False)
|
303
301
|
# cleanly exit
|
304
302
|
sys.exit(0)
|
305
303
|
|
@@ -383,7 +381,6 @@ def initialise_options(
|
|
383
381
|
|
384
382
|
Returns the options dictionary
|
385
383
|
"""
|
386
|
-
|
387
384
|
options: OptionsWritable = InformativeDict(
|
388
385
|
{option["name"]: option["default"] for option in config_metadata.options_config}
|
389
386
|
)
|
@@ -2,9 +2,9 @@ import pathlib
|
|
2
2
|
import sys
|
3
3
|
import typing
|
4
4
|
|
5
|
-
import yaml
|
6
5
|
from packaging.version import Version
|
7
6
|
|
7
|
+
from runem.config_validate import validate_runem_file
|
8
8
|
from runem.log import error, log
|
9
9
|
from runem.runem_version import get_runem_version
|
10
10
|
from runem.types.runem_config import (
|
@@ -13,6 +13,7 @@ from runem.types.runem_config import (
|
|
13
13
|
GlobalSerialisedConfig,
|
14
14
|
UserConfigMetadata,
|
15
15
|
)
|
16
|
+
from runem.yaml_utils import load_yaml_object
|
16
17
|
|
17
18
|
CFG_FILE_YAML = pathlib.Path(".runem.yml")
|
18
19
|
|
@@ -46,7 +47,7 @@ def _search_up_multiple_dirs_for_file(
|
|
46
47
|
|
47
48
|
|
48
49
|
def _find_config_file(
|
49
|
-
config_filename: typing.Union[str, pathlib.Path]
|
50
|
+
config_filename: typing.Union[str, pathlib.Path],
|
50
51
|
) -> typing.Tuple[typing.Optional[pathlib.Path], typing.Tuple[pathlib.Path, ...]]:
|
51
52
|
"""Searches up from the cwd for the given config file-name."""
|
52
53
|
start_dirs = (pathlib.Path(".").absolute(),)
|
@@ -117,8 +118,11 @@ def _conform_global_config_types(
|
|
117
118
|
|
118
119
|
def load_and_parse_config(cfg_filepath: pathlib.Path) -> Config:
|
119
120
|
"""For the given config file pass, project or user, load it & parse/conform it."""
|
120
|
-
|
121
|
-
|
121
|
+
all_config = load_yaml_object(cfg_filepath)
|
122
|
+
validate_runem_file(
|
123
|
+
cfg_filepath,
|
124
|
+
all_config,
|
125
|
+
)
|
122
126
|
|
123
127
|
conformed_config: Config
|
124
128
|
global_config: typing.Optional[GlobalConfig]
|
@@ -162,7 +162,8 @@ def parse_job_config(
|
|
162
162
|
("cwd" in job["ctx"]) and (job["ctx"]["cwd"] is not None)
|
163
163
|
)
|
164
164
|
if (not have_ctw_cwd) or isinstance(
|
165
|
-
job["ctx"]["cwd"],
|
165
|
+
job["ctx"]["cwd"], # type: ignore # handled above
|
166
|
+
str,
|
166
167
|
):
|
167
168
|
# if
|
168
169
|
# - we don't have a cwd, ctx
|
@@ -0,0 +1,47 @@
|
|
1
|
+
import pathlib
|
2
|
+
import typing
|
3
|
+
|
4
|
+
from runem.log import error, log
|
5
|
+
from runem.types.errors import SystemExitBad
|
6
|
+
from runem.yaml_utils import load_yaml_object
|
7
|
+
from runem.yaml_validation import ValidationErrors, validate_yaml
|
8
|
+
|
9
|
+
|
10
|
+
def _load_runem_schema() -> typing.Any:
|
11
|
+
"""Loads and returns the yaml schema for runem.
|
12
|
+
|
13
|
+
Returns:
|
14
|
+
Any: the Draft202012Validator conformant schema.
|
15
|
+
"""
|
16
|
+
schema_path: pathlib.Path = pathlib.Path(__file__).with_name("schema.yml")
|
17
|
+
if not schema_path.exists():
|
18
|
+
error(
|
19
|
+
(
|
20
|
+
"runem schema file not found, cannot continue! "
|
21
|
+
f"Is the install corrupt? {schema_path}"
|
22
|
+
)
|
23
|
+
)
|
24
|
+
raise SystemExitBad(1)
|
25
|
+
schema: typing.Any = load_yaml_object(schema_path)
|
26
|
+
return schema
|
27
|
+
|
28
|
+
|
29
|
+
def validate_runem_file(
|
30
|
+
cfg_filepath: pathlib.Path,
|
31
|
+
all_config: typing.Any,
|
32
|
+
) -> None:
|
33
|
+
"""Validates the config Loader object against the runem schema.
|
34
|
+
|
35
|
+
Exits if the files does not validate.
|
36
|
+
"""
|
37
|
+
schema: typing.Any = _load_runem_schema()
|
38
|
+
errors: ValidationErrors = validate_yaml(all_config, schema)
|
39
|
+
if not errors:
|
40
|
+
# aok
|
41
|
+
return
|
42
|
+
|
43
|
+
error(f"failed to validate runem config [yellow]{cfg_filepath}[/yellow]")
|
44
|
+
for err in errors:
|
45
|
+
path = ".".join(map(str, err.path)) or "<root>"
|
46
|
+
log(f" [yellow]{path}[/yellow]: {err.message}")
|
47
|
+
raise SystemExit("Config validation failed.")
|
@@ -9,8 +9,7 @@ class InformativeDict(typing.Dict[K, V], typing.Generic[K, V]):
|
|
9
9
|
"""A dictionary type that prints out the available keys."""
|
10
10
|
|
11
11
|
def __getitem__(self, key: K) -> V:
|
12
|
-
"""Attempt to
|
13
|
-
found."""
|
12
|
+
"""Attempt to get item, raising a detailed exception if the key is not found."""
|
14
13
|
try:
|
15
14
|
return super().__getitem__(key)
|
16
15
|
except KeyError:
|
@@ -24,19 +23,25 @@ class ReadOnlyInformativeDict(InformativeDict[K, V], typing.Generic[K, V]):
|
|
24
23
|
"""A read-only variant of the above."""
|
25
24
|
|
26
25
|
def __setitem__(self, key: K, value: V) -> None:
|
26
|
+
"""Readonly object, setitem disallowed."""
|
27
27
|
raise NotImplementedError("This dictionary is read-only")
|
28
28
|
|
29
29
|
def __delitem__(self, key: K) -> None:
|
30
|
+
"""Readonly object, delitem disallowed."""
|
30
31
|
raise NotImplementedError("This dictionary is read-only")
|
31
32
|
|
32
33
|
def pop(self, *args: typing.Any, **kwargs: typing.Any) -> V:
|
34
|
+
"""Readonly object, pop disallowed."""
|
33
35
|
raise NotImplementedError("This dictionary is read-only")
|
34
36
|
|
35
37
|
def popitem(self) -> typing.Tuple[K, V]:
|
38
|
+
"""Readonly object, popitem disallowed."""
|
36
39
|
raise NotImplementedError("This dictionary is read-only")
|
37
40
|
|
38
41
|
def clear(self) -> None:
|
42
|
+
"""Readonly object, clear disallowed."""
|
39
43
|
raise NotImplementedError("This dictionary is read-only")
|
40
44
|
|
41
45
|
def update(self, *args: typing.Any, **kwargs: typing.Any) -> None:
|
46
|
+
"""Readonly object, update disallowed."""
|
42
47
|
raise NotImplementedError("This dictionary is read-only")
|
@@ -71,7 +71,6 @@ class Job:
|
|
71
71
|
|
72
72
|
TODO: make a non-static member function
|
73
73
|
"""
|
74
|
-
|
75
74
|
# default to all file-tags
|
76
75
|
tags_for_files: typing.Iterable[str] = file_lists.keys()
|
77
76
|
use_default_tags: bool = job_tags is None
|
@@ -91,7 +90,6 @@ class Job:
|
|
91
90
|
|
92
91
|
TODO: make a non-static member function
|
93
92
|
"""
|
94
|
-
|
95
93
|
# First try one of the following keys.
|
96
94
|
valid_name_keys = ("label", "command")
|
97
95
|
for candidate in valid_name_keys:
|
@@ -101,6 +99,6 @@ class Job:
|
|
101
99
|
|
102
100
|
# The try the python-wrapper address
|
103
101
|
try:
|
104
|
-
return f
|
102
|
+
return f"{job['addr']['file']}.{job['addr']['function']}"
|
105
103
|
except KeyError:
|
106
104
|
raise NoJobName() # pylint: disable=raise-missing-from
|
@@ -105,7 +105,7 @@ def job_execute_inner(
|
|
105
105
|
reports = function(**all_k_args)
|
106
106
|
except BaseException: # pylint: disable=broad-exception-caught
|
107
107
|
# log that we hit an error on this job and re-raise
|
108
|
-
log(
|
108
|
+
log(prefix=False)
|
109
109
|
error(f"job: job '{Job.get_job_name(job_config)}' failed to complete!")
|
110
110
|
# re-raise
|
111
111
|
raise
|
@@ -132,12 +132,15 @@ def job_execute(
|
|
132
132
|
"""
|
133
133
|
this_id: str = str(uuid.uuid4())
|
134
134
|
running_jobs[this_id] = Job.get_job_name(job_config)
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
135
|
+
try:
|
136
|
+
results = job_execute_inner(
|
137
|
+
job_config,
|
138
|
+
config_metadata,
|
139
|
+
file_lists,
|
140
|
+
**kwargs,
|
141
|
+
)
|
142
|
+
finally:
|
143
|
+
# Always tidy-up job statuses
|
144
|
+
completed_jobs[this_id] = running_jobs[this_id]
|
145
|
+
del running_jobs[this_id]
|
143
146
|
return results
|
@@ -101,21 +101,21 @@ def filter_jobs( # noqa: C901
|
|
101
101
|
if tags_to_run:
|
102
102
|
log(
|
103
103
|
f"filtering for tags {printable_set(tags_to_run)}",
|
104
|
-
|
104
|
+
prefix=True,
|
105
105
|
end="",
|
106
106
|
)
|
107
107
|
if tags_to_avoid:
|
108
108
|
if tags_to_run:
|
109
|
-
log(", ",
|
109
|
+
log(", ", prefix=False, end="")
|
110
110
|
else:
|
111
|
-
log(
|
111
|
+
log(prefix=True, end="")
|
112
112
|
log(
|
113
113
|
f"excluding jobs with tags {printable_set(tags_to_avoid)}",
|
114
|
-
|
114
|
+
prefix=False,
|
115
115
|
end="",
|
116
116
|
)
|
117
117
|
if tags_to_run or tags_to_avoid:
|
118
|
-
log(
|
118
|
+
log(prefix=False)
|
119
119
|
filtered_jobs: PhaseGroupedJobs = defaultdict(list)
|
120
120
|
for phase in config_metadata.phases:
|
121
121
|
if phase not in phases_to_run:
|
@@ -15,7 +15,6 @@ def _load_python_function_from_module(
|
|
15
15
|
function_to_load: str,
|
16
16
|
) -> JobFunction:
|
17
17
|
"""Given a job-description dynamically loads the test-function so we can call it."""
|
18
|
-
|
19
18
|
# first locate the module relative to the config file
|
20
19
|
abs_module_file_path: pathlib.Path = (
|
21
20
|
cfg_filepath.parent / module_file_path
|
@@ -109,9 +108,9 @@ def get_job_wrapper_py_func(
|
|
109
108
|
) from err
|
110
109
|
|
111
110
|
anchored_file_path = cfg_filepath.parent / module_file_path
|
112
|
-
assert (
|
113
|
-
anchored_file_path
|
114
|
-
)
|
111
|
+
assert anchored_file_path.exists(), (
|
112
|
+
f"{module_file_path} not found at {anchored_file_path}!"
|
113
|
+
)
|
115
114
|
|
116
115
|
module_name = module_file_path.stem.replace(" ", "_").replace("-", "_")
|
117
116
|
|
@@ -5,7 +5,7 @@ from runem.blocking_print import blocking_print
|
|
5
5
|
|
6
6
|
def log(
|
7
7
|
msg: str = "",
|
8
|
-
|
8
|
+
prefix: typing.Optional[bool] = None,
|
9
9
|
end: typing.Optional[str] = None,
|
10
10
|
) -> None:
|
11
11
|
"""Thin wrapper around 'print', change the 'msg' & handles system-errors.
|
@@ -26,7 +26,10 @@ def log(
|
|
26
26
|
# Remove any markup as it will probably error, if unsanitised.
|
27
27
|
# msg = escape(msg)
|
28
28
|
|
29
|
-
if
|
29
|
+
if prefix is None:
|
30
|
+
prefix = True
|
31
|
+
|
32
|
+
if prefix:
|
30
33
|
# Make it clear that the message comes from `runem` internals.
|
31
34
|
msg = f"[light_slate_grey]runem[/light_slate_grey]: {msg}"
|
32
35
|
|
@@ -46,12 +46,13 @@ def replace_bar_graph_characters(text: str, end_str: str, replace_char: str) ->
|
|
46
46
|
"""Replaces block characters in lines containing `end_str` with give char.
|
47
47
|
|
48
48
|
Args:
|
49
|
-
|
49
|
+
text (str): Text containing lines of bar-graphs (perhaps)
|
50
|
+
end_str (str): If contained by a line, the bar-graph shapes are replaced.
|
50
51
|
replace_char (str): The character to replace all bocks with
|
51
52
|
|
52
53
|
Returns:
|
53
|
-
|
54
|
-
|
54
|
+
str: The modified `text` with block characters replaced on specific
|
55
|
+
lines.
|
55
56
|
"""
|
56
57
|
# Define the block character and its light shade replacement
|
57
58
|
block_chars = (
|
@@ -41,9 +41,7 @@ RecordSubJobTimeType = typing.Callable[[str, timedelta], None]
|
|
41
41
|
|
42
42
|
|
43
43
|
def parse_stdout(stdout: str, prefix: str) -> str:
|
44
|
-
"""Prefixes each line of
|
45
|
-
lines."""
|
46
|
-
|
44
|
+
"""Prefixes each line of output with a given label, except trailing new lines."""
|
47
45
|
# Edge case: Return the prefix immediately for an empty string
|
48
46
|
if not stdout:
|
49
47
|
return prefix
|
@@ -93,13 +91,13 @@ def _log_command_execution(
|
|
93
91
|
if verbose:
|
94
92
|
log(
|
95
93
|
f"running: start: [blue]{label}[/blue]: [yellow]{cmd_string}[yellow]",
|
96
|
-
|
94
|
+
prefix=decorate_logs,
|
97
95
|
)
|
98
96
|
if valid_exit_ids is not None:
|
99
97
|
valid_exit_strs = ",".join(str(exit_code) for exit_code in valid_exit_ids)
|
100
98
|
log(
|
101
99
|
f"\tallowed return ids are: [green]{valid_exit_strs}[/green]",
|
102
|
-
|
100
|
+
prefix=decorate_logs,
|
103
101
|
)
|
104
102
|
|
105
103
|
if env_overrides:
|
@@ -108,11 +106,11 @@ def _log_command_execution(
|
|
108
106
|
)
|
109
107
|
log(
|
110
108
|
f"ENV OVERRIDES: [yellow]{env_overrides_as_string} {cmd_string}[/yellow]",
|
111
|
-
|
109
|
+
prefix=decorate_logs,
|
112
110
|
)
|
113
111
|
|
114
112
|
if cwd:
|
115
|
-
log(f"cwd: {str(cwd)}",
|
113
|
+
log(f"cwd: {str(cwd)}", prefix=decorate_logs)
|
116
114
|
|
117
115
|
|
118
116
|
def run_command( # noqa: C901
|
@@ -175,7 +173,7 @@ def run_command( # noqa: C901
|
|
175
173
|
parse_stdout(
|
176
174
|
line, prefix=f"[green]| [/green][blue]{label}[/blue]: "
|
177
175
|
),
|
178
|
-
|
176
|
+
prefix=False,
|
179
177
|
)
|
180
178
|
|
181
179
|
# Wait for the subprocess to finish and get the exit code
|
@@ -206,7 +204,7 @@ def run_command( # noqa: C901
|
|
206
204
|
error_string = (
|
207
205
|
f"runem: [red bold]FATAL[/red bold]: command failed: [blue]{label}[/blue]"
|
208
206
|
f"\n\t[yellow]{env_overrides_as_string}{cmd_string}[/yellow]"
|
209
|
-
f"\n[red underline]| ERROR[/red underline]"
|
207
|
+
f"\n[red underline]| ERROR[/red underline]: [blue]{label}[/blue]"
|
210
208
|
f"\n{str(parsed_stdout)}"
|
211
209
|
f"\n[red underline]| ERROR END[/red underline]"
|
212
210
|
)
|
@@ -219,7 +217,7 @@ def run_command( # noqa: C901
|
|
219
217
|
if verbose:
|
220
218
|
log(
|
221
219
|
f"running: done: [blue]{label}[/blue]: [yellow]{cmd_string}[/yellow]",
|
222
|
-
|
220
|
+
prefix=decorate_logs,
|
223
221
|
)
|
224
222
|
|
225
223
|
if record_sub_job_time is not None:
|
@@ -19,6 +19,7 @@ We do:
|
|
19
19
|
- time tests and tell you what used the most time, and how much time run-tests saved
|
20
20
|
you
|
21
21
|
"""
|
22
|
+
|
22
23
|
import contextlib
|
23
24
|
import multiprocessing
|
24
25
|
import os
|
@@ -48,6 +49,7 @@ from runem.log import error, log, warn
|
|
48
49
|
from runem.report import report_on_run
|
49
50
|
from runem.run_command import RunemJobError
|
50
51
|
from runem.types.common import OrderedPhases, PhaseName
|
52
|
+
from runem.types.errors import SystemExitBad
|
51
53
|
from runem.types.filters import FilePathListLookup
|
52
54
|
from runem.types.hooks import HookName
|
53
55
|
from runem.types.runem_config import Config, Jobs, PhaseGroupedJobs
|
@@ -68,7 +70,6 @@ def _determine_run_parameters(argv: typing.List[str]) -> ConfigMetadata:
|
|
68
70
|
|
69
71
|
Return a ConfigMetadata object with all the required information.
|
70
72
|
"""
|
71
|
-
|
72
73
|
# Because we want to be able to show logging whilst parsing .runem.yml config, we
|
73
74
|
# need to check the state of the logging-verbosity switches here, manually, as well.
|
74
75
|
verbose = "--verbose" in argv
|
@@ -105,13 +106,14 @@ def _update_progress(
|
|
105
106
|
"""Updates progress report periodically for running tasks.
|
106
107
|
|
107
108
|
Args:
|
108
|
-
|
109
|
+
phase (str): The currently running phase.
|
109
110
|
running_jobs (Dict[str, str]): The currently running jobs.
|
111
|
+
completed_jobs (Dict[str, str]): The jobs that have finished work.
|
110
112
|
all_jobs (Jobs): All jobs, encompassing both completed and running jobs.
|
111
113
|
is_running (ValueProxy[bool]): Flag indicating if jobs are still running.
|
112
114
|
num_workers (int): Indicates the number of workers performing the jobs.
|
115
|
+
show_spinner (bool): Whether to show the animated spinner or not.
|
113
116
|
"""
|
114
|
-
|
115
117
|
last_running_jobs_set: typing.Set[str] = set()
|
116
118
|
|
117
119
|
# Using the `rich` module to show a loading spinner on console
|
@@ -132,7 +134,8 @@ def _update_progress(
|
|
132
134
|
"blue",
|
133
135
|
) # Reflect current running jobs accurately
|
134
136
|
report: str = (
|
135
|
-
f"[green]{phase}[/green]: {progress}({num_workers}):
|
137
|
+
f"[green]{phase}[/green]: {progress}({num_workers}): "
|
138
|
+
f"{running_jobs_list}"
|
136
139
|
)
|
137
140
|
if show_spinner:
|
138
141
|
assert isinstance(spinner_ctx, Status)
|
@@ -293,8 +296,8 @@ def _main(
|
|
293
296
|
log(f"found {len(file_lists)} batches, ", end="")
|
294
297
|
for tag in sorted(file_lists.keys()):
|
295
298
|
file_list = file_lists[tag]
|
296
|
-
log(f"{len(file_list)} '{tag}' files, ",
|
297
|
-
log(
|
299
|
+
log(f"{len(file_list)} '{tag}' files, ", prefix=False, end="")
|
300
|
+
log(prefix=False) # new line
|
298
301
|
|
299
302
|
filtered_jobs_by_phase: PhaseGroupedJobs = filter_jobs(
|
300
303
|
config_metadata=config_metadata,
|
@@ -371,7 +374,7 @@ def timed_main(argv: typing.List[str]) -> None:
|
|
371
374
|
# we got a failure somewhere, now that we've reported the timings we
|
372
375
|
# re-raise.
|
373
376
|
error(failure_exception.stdout)
|
374
|
-
raise failure_exception
|
377
|
+
raise SystemExitBad(1) from failure_exception
|
375
378
|
|
376
379
|
|
377
380
|
if __name__ == "__main__":
|