runem 0.6.0__tar.gz → 0.7.1__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.
Files changed (89) hide show
  1. {runem-0.6.0 → runem-0.7.1}/HISTORY.md +73 -0
  2. {runem-0.6.0 → runem-0.7.1}/PKG-INFO +9 -8
  3. {runem-0.6.0 → runem-0.7.1}/pyproject.toml +8 -6
  4. runem-0.7.1/runem/VERSION +1 -0
  5. {runem-0.6.0 → runem-0.7.1}/runem/blocking_print.py +3 -0
  6. {runem-0.6.0 → runem-0.7.1}/runem/cli/initialise_options.py +0 -1
  7. {runem-0.6.0 → runem-0.7.1}/runem/command_line.py +4 -7
  8. {runem-0.6.0 → runem-0.7.1}/runem/config.py +8 -4
  9. {runem-0.6.0 → runem-0.7.1}/runem/config_parse.py +7 -8
  10. runem-0.7.1/runem/config_validate.py +47 -0
  11. {runem-0.6.0 → runem-0.7.1}/runem/informative_dict.py +7 -2
  12. {runem-0.6.0 → runem-0.7.1}/runem/job.py +1 -3
  13. {runem-0.6.0 → runem-0.7.1}/runem/job_execute.py +12 -9
  14. {runem-0.6.0 → runem-0.7.1}/runem/job_filter.py +5 -5
  15. {runem-0.6.0 → runem-0.7.1}/runem/job_wrapper_python.py +6 -7
  16. {runem-0.6.0 → runem-0.7.1}/runem/log.py +5 -2
  17. {runem-0.6.0 → runem-0.7.1}/runem/report.py +4 -3
  18. {runem-0.6.0 → runem-0.7.1}/runem/run_command.py +8 -10
  19. {runem-0.6.0 → runem-0.7.1}/runem/runem.py +10 -7
  20. runem-0.7.1/runem/schema.yml +137 -0
  21. runem-0.7.1/runem/types/errors.py +14 -0
  22. {runem-0.6.0 → runem-0.7.1}/runem/types/hooks.py +1 -1
  23. {runem-0.6.0 → runem-0.7.1}/runem/types/types_jobs.py +21 -24
  24. runem-0.7.1/runem/yaml_utils.py +19 -0
  25. runem-0.7.1/runem/yaml_validation.py +28 -0
  26. {runem-0.6.0 → runem-0.7.1}/runem.egg-info/PKG-INFO +9 -8
  27. {runem-0.6.0 → runem-0.7.1}/runem.egg-info/SOURCES.txt +6 -0
  28. {runem-0.6.0 → runem-0.7.1}/runem.egg-info/requires.txt +6 -6
  29. {runem-0.6.0 → runem-0.7.1}/scripts/test_hooks/py.py +63 -1
  30. {runem-0.6.0 → runem-0.7.1}/tests/test_config_parse.py +8 -5
  31. runem-0.7.1/tests/test_config_validate.py +128 -0
  32. {runem-0.6.0 → runem-0.7.1}/tests/test_files.py +3 -3
  33. {runem-0.6.0 → runem-0.7.1}/tests/test_hook_manager.py +3 -2
  34. {runem-0.6.0 → runem-0.7.1}/tests/test_informative_dict.py +21 -20
  35. {runem-0.6.0 → runem-0.7.1}/tests/test_job_filter.py +0 -2
  36. {runem-0.6.0 → runem-0.7.1}/tests/test_job_runner_simple_command.py +2 -1
  37. {runem-0.6.0 → runem-0.7.1}/tests/test_job_wrapper.py +2 -3
  38. {runem-0.6.0 → runem-0.7.1}/tests/test_run_command.py +10 -11
  39. {runem-0.6.0 → runem-0.7.1}/tests/test_runem.py +18 -16
  40. runem-0.7.1/tests/test_yaml_validation.py +66 -0
  41. runem-0.6.0/runem/VERSION +0 -1
  42. runem-0.6.0/runem/types/errors.py +0 -4
  43. {runem-0.6.0 → runem-0.7.1}/Containerfile +0 -0
  44. {runem-0.6.0 → runem-0.7.1}/LICENSE +0 -0
  45. {runem-0.6.0 → runem-0.7.1}/MANIFEST.in +0 -0
  46. {runem-0.6.0 → runem-0.7.1}/README.md +0 -0
  47. {runem-0.6.0 → runem-0.7.1}/runem/__init__.py +0 -0
  48. {runem-0.6.0 → runem-0.7.1}/runem/__main__.py +0 -0
  49. {runem-0.6.0 → runem-0.7.1}/runem/base.py +0 -0
  50. {runem-0.6.0 → runem-0.7.1}/runem/cli.py +0 -0
  51. {runem-0.6.0 → runem-0.7.1}/runem/config_metadata.py +0 -0
  52. {runem-0.6.0 → runem-0.7.1}/runem/files.py +0 -0
  53. {runem-0.6.0 → runem-0.7.1}/runem/hook_manager.py +0 -0
  54. {runem-0.6.0 → runem-0.7.1}/runem/job_runner_simple_command.py +0 -0
  55. {runem-0.6.0 → runem-0.7.1}/runem/job_wrapper.py +0 -0
  56. {runem-0.6.0 → runem-0.7.1}/runem/py.typed +0 -0
  57. {runem-0.6.0 → runem-0.7.1}/runem/runem_version.py +0 -0
  58. {runem-0.6.0 → runem-0.7.1}/runem/types/__init__.py +0 -0
  59. {runem-0.6.0 → runem-0.7.1}/runem/types/common.py +0 -0
  60. {runem-0.6.0 → runem-0.7.1}/runem/types/filters.py +0 -0
  61. {runem-0.6.0 → runem-0.7.1}/runem/types/options.py +0 -0
  62. {runem-0.6.0 → runem-0.7.1}/runem/types/runem_config.py +0 -0
  63. {runem-0.6.0 → runem-0.7.1}/runem/utils.py +0 -0
  64. {runem-0.6.0 → runem-0.7.1}/runem.egg-info/dependency_links.txt +0 -0
  65. {runem-0.6.0 → runem-0.7.1}/runem.egg-info/entry_points.txt +0 -0
  66. {runem-0.6.0 → runem-0.7.1}/runem.egg-info/top_level.txt +0 -0
  67. {runem-0.6.0 → runem-0.7.1}/scripts/test_hooks/__init__.py +0 -0
  68. {runem-0.6.0 → runem-0.7.1}/scripts/test_hooks/json_validators.py +0 -0
  69. {runem-0.6.0 → runem-0.7.1}/scripts/test_hooks/py.typed +0 -0
  70. {runem-0.6.0 → runem-0.7.1}/scripts/test_hooks/runem_hooks.py +0 -0
  71. {runem-0.6.0 → runem-0.7.1}/scripts/test_hooks/yarn.py +0 -0
  72. {runem-0.6.0 → runem-0.7.1}/setup.cfg +0 -0
  73. {runem-0.6.0 → runem-0.7.1}/tests/__init__.py +0 -0
  74. {runem-0.6.0 → runem-0.7.1}/tests/cli/test_initialise_options.py +0 -0
  75. {runem-0.6.0 → runem-0.7.1}/tests/conftest.py +0 -0
  76. {runem-0.6.0 → runem-0.7.1}/tests/data/help_output.3.10.txt +0 -0
  77. {runem-0.6.0 → runem-0.7.1}/tests/data/help_output.3.11.txt +0 -0
  78. {runem-0.6.0 → runem-0.7.1}/tests/intentional_test_error.py +0 -0
  79. {runem-0.6.0 → runem-0.7.1}/tests/sanitise_reports_footer.py +0 -0
  80. {runem-0.6.0 → runem-0.7.1}/tests/test_base.py +0 -0
  81. {runem-0.6.0 → runem-0.7.1}/tests/test_blocking_print.py +0 -0
  82. {runem-0.6.0 → runem-0.7.1}/tests/test_cli.py +0 -0
  83. {runem-0.6.0 → runem-0.7.1}/tests/test_config.py +0 -0
  84. {runem-0.6.0 → runem-0.7.1}/tests/test_job.py +0 -0
  85. {runem-0.6.0 → runem-0.7.1}/tests/test_job_execute.py +0 -0
  86. {runem-0.6.0 → runem-0.7.1}/tests/test_job_wrapper_python.py +0 -0
  87. {runem-0.6.0 → runem-0.7.1}/tests/test_report.py +0 -0
  88. {runem-0.6.0 → runem-0.7.1}/tests/test_types/__init__.py +0 -0
  89. {runem-0.6.0 → runem-0.7.1}/tests/test_types/test_public_api.py +0 -0
@@ -4,6 +4,79 @@ Changelog
4
4
 
5
5
  (unreleased)
6
6
  ------------
7
+ - Merge pull request #85 from lursight/feat/better_function-not-
8
+ found_error. [Frank Harrison]
9
+
10
+ Feat/better function not found error
11
+ - Feat(function-lookup-error): exits instead of reraising errors looking
12
+ for functions. [Frank Harrison]
13
+ - Feat(coloured-function-lookups): colours the function errors. [Frank
14
+ Harrison]
15
+ - Merge pull request #84 from lursight/fix/log-wrapping. [Frank
16
+ Harrison]
17
+
18
+ fix(log-wrapping): fixes log wrapping so it does not truncate
19
+ - Fix(log-wrapping): fixes log wrapping so it does not truncate. [Frank
20
+ Harrison]
21
+
22
+ I used printing of multiple variants to find this switch. This happened
23
+ because `soft_wrap=True` implies that it is turning on a line-break or
24
+ similar system *instead of* turning off hard-wrapping, so it should be
25
+ `hard_wrap=False` or similar.
26
+
27
+
28
+ 0.7.0 (2025-04-21)
29
+ ------------------
30
+ - Release: version 0.7.0 🚀 [Frank Harrison]
31
+ - Merge pull request #83 from
32
+ lursight/feat/no_runem_traceback_on_job_fail. [Frank Harrison]
33
+
34
+ Feat/no runem traceback on job fail
35
+ - Feat(error-only): only shows the error not the runem error tracback.
36
+ [Frank Harrison]
37
+
38
+ The runem error traceback is irelevant if the sub-task fails. So we just
39
+ show that task's output instead of the traceback for where we handle the
40
+ error.
41
+ - Feat(error-ctx): shows the failed job label as we show the causing
42
+ error. [Frank Harrison]
43
+ - Feat(remove-failed): always remove the failed job from the list of
44
+ running jobs. [Frank Harrison]
45
+ - Merge pull request #82 from lursight/feat/schema_validation. [Frank
46
+ Harrison]
47
+
48
+ feat(validation): validates the .runem.yml file against the schema
49
+ - Feat(validation): validates the .runem.yml file against the schema.
50
+ [Frank Harrison]
51
+ - Merge pull request #81 from lursight/chore/ruff. [Frank Harrison]
52
+
53
+ Chore/ruff
54
+ - Chore(ruff): some formatting change made whilst configuring ruff.
55
+ [Frank Harrison]
56
+ - Chore(ruff): use ruff as its faster/better. [Frank Harrison]
57
+ - Merge pull request #80 from lursight/chore/update_deps. [Frank
58
+ Harrison]
59
+
60
+ Chore/update deps
61
+ - Chore(deps): updates pylint 3.1.0 -> 3.3.6. [Frank Harrison]
62
+ - Chore(deps): updates pytest 8.3.3 -> 8.3.5 and pytest-cov to latest.
63
+ [Frank Harrison]
64
+ - Merge pull request #79 from
65
+ lursight/feat/removes_dectorate_param_from_log. [Frank Harrison]
66
+
67
+ feat(log): changes the semantics of log's 'decorate' to allow overriding
68
+ - Feat(log): changes the semantics of log's 'decorate' to allow
69
+ overriding. [Frank Harrison]
70
+
71
+ Also renames the log API's param decorate -> prefix.
72
+
73
+ This better represents the intent of the param as decorate was adding a
74
+ default prefix.
75
+
76
+
77
+ 0.6.0 (2025-02-03)
78
+ ------------------
79
+ - Release: version 0.6.0 🚀 [Frank Harrison]
7
80
  - Merge pull request #78 from lursight/feat/better_error_display. [Frank
8
81
  Harrison]
9
82
 
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.2
1
+ Metadata-Version: 2.4
2
2
  Name: runem
3
- Version: 0.6.0
3
+ Version: 0.7.1
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.1.0; extra == "tests"
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.0.0; extra == "tests"
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.3; extra == "tests"
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
  <!-- [![codecov](https://codecov.io/gh/lursight/runem/branch/main/graph/badge.svg?token=run-test_token_here)](https://codecov.io/gh/lursight/runem) -->
46
47
  [![CI](https://github.com/lursight/runem/actions/workflows/main.yml/badge.svg)](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.1.0",
62
+ "pylint==3.3.6",
63
63
  "pylama==8.4.1",
64
- "pytest-cov==6.0.0",
64
+ "pytest-cov==6.1.1",
65
65
  "pytest-profiling==1.7.0",
66
66
  "pytest-xdist==3.6.1",
67
- "pytest==8.3.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.1
@@ -21,6 +21,9 @@ def _reset_console() -> Console:
21
21
  # `highlight` is what colourises string and number in print() calls.
22
22
  # We do not want this to be auto-magic.
23
23
  highlight=False,
24
+ # `soft_wrap=True` disables word-wrap & cropping by default:
25
+ # - `soft_wrap` reads like a misnomer to me
26
+ soft_wrap=True,
24
27
  )
25
28
  return RICH_CONSOLE
26
29
 
@@ -13,7 +13,6 @@ def initialise_options(
13
13
 
14
14
  Returns the options dictionary
15
15
  """
16
-
17
16
  options: OptionsWritable = InformativeDict(
18
17
  {option["name"]: option["default"] for option in config_metadata.options_config}
19
18
  )
@@ -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
- lambda prog: HelpFormatterFixedWidth( # pylint: disable=unnecessary-lambda
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), decorate=False)
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()), decorate=False)
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
- with cfg_filepath.open("r+", encoding="utf-8") as config_file_handle:
121
- all_config = yaml.full_load(config_file_handle)
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]
@@ -11,7 +11,7 @@ from runem.job import Job
11
11
  from runem.job_wrapper import get_job_wrapper
12
12
  from runem.log import error, log, warn
13
13
  from runem.types.common import JobNames, JobPhases, JobTags, OrderedPhases, PhaseName
14
- from runem.types.errors import FunctionNotFound
14
+ from runem.types.errors import FunctionNotFound, SystemExitBad
15
15
  from runem.types.filters import TagFileFilter, TagFileFilters
16
16
  from runem.types.hooks import HookName
17
17
  from runem.types.runem_config import (
@@ -81,9 +81,8 @@ def parse_hook_config(
81
81
  f"hook config entry is missing '{err.args[0]}' key. Have {tuple(hook.keys())}"
82
82
  ) from err
83
83
  except FunctionNotFound as err:
84
- raise FunctionNotFound(
85
- f"Whilst loading job '{str(hook['hook_name'])}'. {str(err)}"
86
- ) from err
84
+ error(f"Whilst loading hook '{str(hook['hook_name'])}'. {str(err)}")
85
+ raise SystemExitBad(2) from err
87
86
 
88
87
 
89
88
  def _parse_job( # noqa: C901
@@ -110,9 +109,8 @@ def _parse_job( # noqa: C901
110
109
  # try and load the function _before_ we schedule it's execution
111
110
  get_job_wrapper(job, cfg_filepath)
112
111
  except FunctionNotFound as err:
113
- raise FunctionNotFound(
114
- f"Whilst loading job '{job['label']}'. {str(err)}"
115
- ) from err
112
+ error(f"Whilst loading job '{job['label']}'. {str(err)}")
113
+ raise SystemExitBad(2) from err
116
114
 
117
115
  try:
118
116
  phase_id: PhaseName = job["when"]["phase"]
@@ -162,7 +160,8 @@ def parse_job_config(
162
160
  ("cwd" in job["ctx"]) and (job["ctx"]["cwd"] is not None)
163
161
  )
164
162
  if (not have_ctw_cwd) or isinstance(
165
- job["ctx"]["cwd"], str # type: ignore # handled above
163
+ job["ctx"]["cwd"], # type: ignore # handled above
164
+ str,
166
165
  ):
167
166
  # if
168
167
  # - 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 retrieve an item, raising a detailed exception if the key is not
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'{job["addr"]["file"]}.{job["addr"]["function"]}'
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(decorate=False)
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
- results = job_execute_inner(
136
- job_config,
137
- config_metadata,
138
- file_lists,
139
- **kwargs,
140
- )
141
- completed_jobs[this_id] = running_jobs[this_id]
142
- del running_jobs[this_id]
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
- decorate=True,
104
+ prefix=True,
105
105
  end="",
106
106
  )
107
107
  if tags_to_avoid:
108
108
  if tags_to_run:
109
- log(", ", decorate=False, end="")
109
+ log(", ", prefix=False, end="")
110
110
  else:
111
- log(decorate=True, end="")
111
+ log(prefix=True, end="")
112
112
  log(
113
113
  f"excluding jobs with tags {printable_set(tags_to_avoid)}",
114
- decorate=False,
114
+ prefix=False,
115
115
  end="",
116
116
  )
117
117
  if tags_to_run or tags_to_avoid:
118
- log(decorate=False)
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
@@ -56,9 +55,9 @@ def _load_python_function_from_module(
56
55
  except AttributeError as err:
57
56
  raise FunctionNotFound(
58
57
  (
59
- f"ERROR! Check that function '{function_to_load}' "
60
- f"exists in '{str(module_file_path)}' as expected in "
61
- f"your config at '{str(cfg_filepath)}"
58
+ f"Check that function '[blue]{function_to_load}[/blue]' "
59
+ f"exists in '[blue]{str(module_file_path)}[/blue]' as expected in "
60
+ f"your config at '[blue]{str(cfg_filepath)}[/blue]'"
62
61
  )
63
62
  ) from err
64
63
  return function
@@ -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.exists()
114
- ), f"{module_file_path} not found at {anchored_file_path}!"
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
- decorate: bool = True,
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 decorate:
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
- text_lines (List[str]): A list of strings, each representing a line of text.
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
- List[str]: The modified list of strings with block characters replaced
54
- on specified lines.
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 the output with a given label, except trailing new
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
- decorate=decorate_logs,
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
- decorate=decorate_logs,
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
- decorate=decorate_logs,
109
+ prefix=decorate_logs,
112
110
  )
113
111
 
114
112
  if cwd:
115
- log(f"cwd: {str(cwd)}", decorate=decorate_logs)
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
- decorate=False,
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
- decorate=decorate_logs,
220
+ prefix=decorate_logs,
223
221
  )
224
222
 
225
223
  if record_sub_job_time is not None: