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.
Files changed (89) hide show
  1. {runem-0.6.0 → runem-0.7.0}/HISTORY.md +49 -0
  2. {runem-0.6.0 → runem-0.7.0}/PKG-INFO +9 -8
  3. {runem-0.6.0 → runem-0.7.0}/pyproject.toml +8 -6
  4. runem-0.7.0/runem/VERSION +1 -0
  5. {runem-0.6.0 → runem-0.7.0}/runem/cli/initialise_options.py +0 -1
  6. {runem-0.6.0 → runem-0.7.0}/runem/command_line.py +4 -7
  7. {runem-0.6.0 → runem-0.7.0}/runem/config.py +8 -4
  8. {runem-0.6.0 → runem-0.7.0}/runem/config_parse.py +2 -1
  9. runem-0.7.0/runem/config_validate.py +47 -0
  10. {runem-0.6.0 → runem-0.7.0}/runem/informative_dict.py +7 -2
  11. {runem-0.6.0 → runem-0.7.0}/runem/job.py +1 -3
  12. {runem-0.6.0 → runem-0.7.0}/runem/job_execute.py +12 -9
  13. {runem-0.6.0 → runem-0.7.0}/runem/job_filter.py +5 -5
  14. {runem-0.6.0 → runem-0.7.0}/runem/job_wrapper_python.py +3 -4
  15. {runem-0.6.0 → runem-0.7.0}/runem/log.py +5 -2
  16. {runem-0.6.0 → runem-0.7.0}/runem/report.py +4 -3
  17. {runem-0.6.0 → runem-0.7.0}/runem/run_command.py +8 -10
  18. {runem-0.6.0 → runem-0.7.0}/runem/runem.py +10 -7
  19. runem-0.7.0/runem/schema.yml +137 -0
  20. runem-0.7.0/runem/types/errors.py +14 -0
  21. {runem-0.6.0 → runem-0.7.0}/runem/types/hooks.py +1 -1
  22. {runem-0.6.0 → runem-0.7.0}/runem/types/types_jobs.py +21 -24
  23. runem-0.7.0/runem/yaml_utils.py +19 -0
  24. runem-0.7.0/runem/yaml_validation.py +28 -0
  25. {runem-0.6.0 → runem-0.7.0}/runem.egg-info/PKG-INFO +9 -8
  26. {runem-0.6.0 → runem-0.7.0}/runem.egg-info/SOURCES.txt +6 -0
  27. {runem-0.6.0 → runem-0.7.0}/runem.egg-info/requires.txt +6 -6
  28. {runem-0.6.0 → runem-0.7.0}/scripts/test_hooks/py.py +63 -1
  29. runem-0.7.0/tests/test_config_validate.py +128 -0
  30. {runem-0.6.0 → runem-0.7.0}/tests/test_files.py +3 -3
  31. {runem-0.6.0 → runem-0.7.0}/tests/test_hook_manager.py +3 -2
  32. {runem-0.6.0 → runem-0.7.0}/tests/test_informative_dict.py +21 -20
  33. {runem-0.6.0 → runem-0.7.0}/tests/test_job_filter.py +0 -2
  34. {runem-0.6.0 → runem-0.7.0}/tests/test_job_runner_simple_command.py +2 -1
  35. {runem-0.6.0 → runem-0.7.0}/tests/test_job_wrapper.py +2 -3
  36. {runem-0.6.0 → runem-0.7.0}/tests/test_run_command.py +10 -11
  37. {runem-0.6.0 → runem-0.7.0}/tests/test_runem.py +18 -16
  38. runem-0.7.0/tests/test_yaml_validation.py +66 -0
  39. runem-0.6.0/runem/VERSION +0 -1
  40. runem-0.6.0/runem/types/errors.py +0 -4
  41. {runem-0.6.0 → runem-0.7.0}/Containerfile +0 -0
  42. {runem-0.6.0 → runem-0.7.0}/LICENSE +0 -0
  43. {runem-0.6.0 → runem-0.7.0}/MANIFEST.in +0 -0
  44. {runem-0.6.0 → runem-0.7.0}/README.md +0 -0
  45. {runem-0.6.0 → runem-0.7.0}/runem/__init__.py +0 -0
  46. {runem-0.6.0 → runem-0.7.0}/runem/__main__.py +0 -0
  47. {runem-0.6.0 → runem-0.7.0}/runem/base.py +0 -0
  48. {runem-0.6.0 → runem-0.7.0}/runem/blocking_print.py +0 -0
  49. {runem-0.6.0 → runem-0.7.0}/runem/cli.py +0 -0
  50. {runem-0.6.0 → runem-0.7.0}/runem/config_metadata.py +0 -0
  51. {runem-0.6.0 → runem-0.7.0}/runem/files.py +0 -0
  52. {runem-0.6.0 → runem-0.7.0}/runem/hook_manager.py +0 -0
  53. {runem-0.6.0 → runem-0.7.0}/runem/job_runner_simple_command.py +0 -0
  54. {runem-0.6.0 → runem-0.7.0}/runem/job_wrapper.py +0 -0
  55. {runem-0.6.0 → runem-0.7.0}/runem/py.typed +0 -0
  56. {runem-0.6.0 → runem-0.7.0}/runem/runem_version.py +0 -0
  57. {runem-0.6.0 → runem-0.7.0}/runem/types/__init__.py +0 -0
  58. {runem-0.6.0 → runem-0.7.0}/runem/types/common.py +0 -0
  59. {runem-0.6.0 → runem-0.7.0}/runem/types/filters.py +0 -0
  60. {runem-0.6.0 → runem-0.7.0}/runem/types/options.py +0 -0
  61. {runem-0.6.0 → runem-0.7.0}/runem/types/runem_config.py +0 -0
  62. {runem-0.6.0 → runem-0.7.0}/runem/utils.py +0 -0
  63. {runem-0.6.0 → runem-0.7.0}/runem.egg-info/dependency_links.txt +0 -0
  64. {runem-0.6.0 → runem-0.7.0}/runem.egg-info/entry_points.txt +0 -0
  65. {runem-0.6.0 → runem-0.7.0}/runem.egg-info/top_level.txt +0 -0
  66. {runem-0.6.0 → runem-0.7.0}/scripts/test_hooks/__init__.py +0 -0
  67. {runem-0.6.0 → runem-0.7.0}/scripts/test_hooks/json_validators.py +0 -0
  68. {runem-0.6.0 → runem-0.7.0}/scripts/test_hooks/py.typed +0 -0
  69. {runem-0.6.0 → runem-0.7.0}/scripts/test_hooks/runem_hooks.py +0 -0
  70. {runem-0.6.0 → runem-0.7.0}/scripts/test_hooks/yarn.py +0 -0
  71. {runem-0.6.0 → runem-0.7.0}/setup.cfg +0 -0
  72. {runem-0.6.0 → runem-0.7.0}/tests/__init__.py +0 -0
  73. {runem-0.6.0 → runem-0.7.0}/tests/cli/test_initialise_options.py +0 -0
  74. {runem-0.6.0 → runem-0.7.0}/tests/conftest.py +0 -0
  75. {runem-0.6.0 → runem-0.7.0}/tests/data/help_output.3.10.txt +0 -0
  76. {runem-0.6.0 → runem-0.7.0}/tests/data/help_output.3.11.txt +0 -0
  77. {runem-0.6.0 → runem-0.7.0}/tests/intentional_test_error.py +0 -0
  78. {runem-0.6.0 → runem-0.7.0}/tests/sanitise_reports_footer.py +0 -0
  79. {runem-0.6.0 → runem-0.7.0}/tests/test_base.py +0 -0
  80. {runem-0.6.0 → runem-0.7.0}/tests/test_blocking_print.py +0 -0
  81. {runem-0.6.0 → runem-0.7.0}/tests/test_cli.py +0 -0
  82. {runem-0.6.0 → runem-0.7.0}/tests/test_config.py +0 -0
  83. {runem-0.6.0 → runem-0.7.0}/tests/test_config_parse.py +0 -0
  84. {runem-0.6.0 → runem-0.7.0}/tests/test_job.py +0 -0
  85. {runem-0.6.0 → runem-0.7.0}/tests/test_job_execute.py +0 -0
  86. {runem-0.6.0 → runem-0.7.0}/tests/test_job_wrapper_python.py +0 -0
  87. {runem-0.6.0 → runem-0.7.0}/tests/test_report.py +0 -0
  88. {runem-0.6.0 → runem-0.7.0}/tests/test_types/__init__.py +0 -0
  89. {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.2
1
+ Metadata-Version: 2.4
2
2
  Name: runem
3
- Version: 0.6.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.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.0
@@ -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]
@@ -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"], str # type: ignore # handled above
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 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
@@ -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:
@@ -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
- label (str): The identifier.
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}): {running_jobs_list}"
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, ", decorate=False, end="")
297
- log(decorate=False) # new line
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__":