runem 0.0.27__tar.gz → 0.0.29__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 (65) hide show
  1. {runem-0.0.27 → runem-0.0.29}/HISTORY.md +178 -0
  2. {runem-0.0.27 → runem-0.0.29}/PKG-INFO +8 -7
  3. {runem-0.0.27 → runem-0.0.29}/README.md +6 -6
  4. runem-0.0.29/runem/VERSION +1 -0
  5. {runem-0.0.27 → runem-0.0.29}/runem/cli/initialise_options.py +6 -5
  6. {runem-0.0.27 → runem-0.0.29}/runem/command_line.py +7 -6
  7. {runem-0.0.27 → runem-0.0.29}/runem/config_metadata.py +5 -4
  8. runem-0.0.29/runem/informative_dict.py +42 -0
  9. {runem-0.0.27 → runem-0.0.29}/runem/job_execute.py +55 -24
  10. {runem-0.0.27 → runem-0.0.29}/runem/job_filter.py +16 -3
  11. runem-0.0.29/runem/report.py +252 -0
  12. {runem-0.0.27 → runem-0.0.29}/runem/run_command.py +42 -4
  13. {runem-0.0.27 → runem-0.0.29}/runem/runem.py +20 -7
  14. {runem-0.0.27 → runem-0.0.29}/runem/types.py +21 -3
  15. {runem-0.0.27 → runem-0.0.29}/runem.egg-info/PKG-INFO +8 -7
  16. {runem-0.0.27 → runem-0.0.29}/runem.egg-info/SOURCES.txt +4 -1
  17. {runem-0.0.27 → runem-0.0.29}/runem.egg-info/requires.txt +1 -0
  18. runem-0.0.29/tests/data/help_output.3.11.txt +71 -0
  19. runem-0.0.29/tests/test_informative_dict.py +99 -0
  20. {runem-0.0.27 → runem-0.0.29}/tests/test_job_execute.py +101 -23
  21. {runem-0.0.27 → runem-0.0.29}/tests/test_job_filter.py +19 -9
  22. {runem-0.0.27 → runem-0.0.29}/tests/test_report.py +72 -29
  23. {runem-0.0.27 → runem-0.0.29}/tests/test_run_command.py +67 -7
  24. {runem-0.0.27 → runem-0.0.29}/tests/test_runem.py +45 -14
  25. runem-0.0.27/runem/VERSION +0 -1
  26. runem-0.0.27/runem/report.py +0 -145
  27. {runem-0.0.27 → runem-0.0.29}/Containerfile +0 -0
  28. {runem-0.0.27 → runem-0.0.29}/LICENSE +0 -0
  29. {runem-0.0.27 → runem-0.0.29}/MANIFEST.in +0 -0
  30. {runem-0.0.27 → runem-0.0.29}/runem/__init__.py +0 -0
  31. {runem-0.0.27 → runem-0.0.29}/runem/__main__.py +0 -0
  32. {runem-0.0.27 → runem-0.0.29}/runem/base.py +0 -0
  33. {runem-0.0.27 → runem-0.0.29}/runem/blocking_print.py +0 -0
  34. {runem-0.0.27 → runem-0.0.29}/runem/cli.py +0 -0
  35. {runem-0.0.27 → runem-0.0.29}/runem/config.py +0 -0
  36. {runem-0.0.27 → runem-0.0.29}/runem/config_parse.py +0 -0
  37. {runem-0.0.27 → runem-0.0.29}/runem/files.py +0 -0
  38. {runem-0.0.27 → runem-0.0.29}/runem/job.py +0 -0
  39. {runem-0.0.27 → runem-0.0.29}/runem/job_runner_simple_command.py +0 -0
  40. {runem-0.0.27 → runem-0.0.29}/runem/job_wrapper.py +0 -0
  41. {runem-0.0.27 → runem-0.0.29}/runem/job_wrapper_python.py +0 -0
  42. {runem-0.0.27 → runem-0.0.29}/runem/log.py +0 -0
  43. {runem-0.0.27 → runem-0.0.29}/runem/py.typed +0 -0
  44. {runem-0.0.27 → runem-0.0.29}/runem/runem_version.py +0 -0
  45. {runem-0.0.27 → runem-0.0.29}/runem/utils.py +0 -0
  46. {runem-0.0.27 → runem-0.0.29}/runem.egg-info/dependency_links.txt +0 -0
  47. {runem-0.0.27 → runem-0.0.29}/runem.egg-info/entry_points.txt +0 -0
  48. {runem-0.0.27 → runem-0.0.29}/runem.egg-info/top_level.txt +0 -0
  49. {runem-0.0.27 → runem-0.0.29}/setup.cfg +0 -0
  50. {runem-0.0.27 → runem-0.0.29}/setup.py +0 -0
  51. {runem-0.0.27 → runem-0.0.29}/tests/__init__.py +0 -0
  52. {runem-0.0.27 → runem-0.0.29}/tests/cli/test_initialise_options.py +0 -0
  53. {runem-0.0.27 → runem-0.0.29}/tests/conftest.py +0 -0
  54. /runem-0.0.27/tests/data/help_output.txt → /runem-0.0.29/tests/data/help_output.3.10.txt +0 -0
  55. {runem-0.0.27 → runem-0.0.29}/tests/intentional_test_error.py +0 -0
  56. {runem-0.0.27 → runem-0.0.29}/tests/test_base.py +0 -0
  57. {runem-0.0.27 → runem-0.0.29}/tests/test_blocking_print.py +0 -0
  58. {runem-0.0.27 → runem-0.0.29}/tests/test_cli.py +0 -0
  59. {runem-0.0.27 → runem-0.0.29}/tests/test_config.py +0 -0
  60. {runem-0.0.27 → runem-0.0.29}/tests/test_config_parse.py +0 -0
  61. {runem-0.0.27 → runem-0.0.29}/tests/test_files.py +0 -0
  62. {runem-0.0.27 → runem-0.0.29}/tests/test_job.py +0 -0
  63. {runem-0.0.27 → runem-0.0.29}/tests/test_job_runner_simple_command.py +0 -0
  64. {runem-0.0.27 → runem-0.0.29}/tests/test_job_wrapper.py +0 -0
  65. {runem-0.0.27 → runem-0.0.29}/tests/test_job_wrapper_python.py +0 -0
@@ -4,6 +4,184 @@ Changelog
4
4
 
5
5
  (unreleased)
6
6
  ------------
7
+ - Merge pull request #40 from lursight/fix/time_saved. [Frank Harrison]
8
+
9
+ Fix/time saved
10
+ - Feat(prettier-bars): clarifies that total is user-space time. [Frank
11
+ Harrison]
12
+
13
+ ... not wall-clock or system-time
14
+ - Feat(prettier-bars): distiguishes the wall-clock bars. [Frank
15
+ Harrison]
16
+
17
+ ... from the total/sum and sub-job bars, so that it's slightly easier to
18
+ see where the time is being really spent.
19
+ - Fix(time-saved): clarifies which measurement is the wall-clock time
20
+ for the entire run. [Frank Harrison]
21
+ - Fix(time-saved): add message about how long we _would_ have waited
22
+ without runem. [Frank Harrison]
23
+ - Fix(time-saved): renames all variable associated with timing reports.
24
+ [Frank Harrison]
25
+
26
+ This just makes someting which can become intractable/confusing a lot
27
+ easier to follow.
28
+ - Fix(time-saved): check that time-saved is reported correctly. [Frank
29
+ Harrison]
30
+
31
+ Here we add a test first and then fix the missing math to calculate the
32
+ time-saved by using runem. We broke this in the previous feature for
33
+ rendering the tree slightly more elegantly.
34
+ - Feat(hide-single-leafs): only show the job when it has a single child.
35
+ [Frank Harrison]
36
+
37
+ We would get duplicated information for jobs which had single
38
+ run_command invocations. This only shows sub-tasks/jobs if there are
39
+ more than one sub-tasks meaning the output looks a lot nicer & clearer.
40
+ - Chore(deps): adds setuptools as a explicit dep. [Frank Harrison]
41
+
42
+ ... otherwise we get the following error (more often in python 3.12),
43
+ perhaps due to setuptools being removed from distros?:
44
+
45
+ ```text
46
+ Traceback (most recent call last):
47
+ File "/var/www/mydir/virtualenvs/dev/bin/pip", line 5, in <module>
48
+ from pkg_resources import load_entry_point
49
+ ImportError: No module named pkg_resources
50
+ ```
51
+ - Merge pull request #39 from lursight/feat/time_all_run_command_calls.
52
+ [Frank Harrison]
53
+
54
+ Feat/time all run command calls
55
+ - Feat(pretty-tree): refactors out the phase-job report generator.
56
+ [Frank Harrison]
57
+
58
+ This is just to make pylint happy.
59
+ - Feat(pretty-tree): makes the report tree neater. [Frank Harrison]
60
+ - Feat(time-all-sub-tasks): re-raise errors for context in ci/cd. [Frank
61
+ Harrison]
62
+
63
+ In github ci/cd we were hitting the asserts but had no context of where
64
+ they're raised from or why. This should fix that if they still occur.
65
+ - Feat(time-all-sub-tasks): adds a test to test the time-recording
66
+ functions. [Frank Harrison]
67
+ - Feat(time-all-sub-tasks): adds all run_command times to report output.
68
+ [Frank Harrison]
69
+ - Chore(type): uses a type-alias instead of manual type. [Frank
70
+ Harrison]
71
+ - Merge pull request #37 from
72
+ lursight/dependabot/github_actions/actions/upload-artifact-4. [Frank
73
+ Harrison]
74
+
75
+ chore(deps): bump actions/upload-artifact from 3 to 4
76
+ - Chore(deps): bump actions/upload-artifact from 3 to 4.
77
+ [dependabot[bot]]
78
+
79
+ Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 3 to 4.
80
+ - [Release notes](https://github.com/actions/upload-artifact/releases)
81
+ - [Commits](https://github.com/actions/upload-artifact/compare/v3...v4)
82
+
83
+ ---
84
+ updated-dependencies:
85
+ - dependency-name: actions/upload-artifact
86
+ dependency-type: direct:production
87
+ update-type: version-update:semver-major
88
+ ...
89
+ - Merge pull request #36 from lursight/chore/more_python_versions_in_ci.
90
+ [Frank Harrison]
91
+
92
+ Chore/more python versions in ci
93
+ - Chore(github-ci): workaround for python 3.12 setuptools issue. [Frank
94
+ Harrison]
95
+
96
+ This fies what looks like an issue with pytest hooks running ci/cd (and
97
+ on local machine) where we get:
98
+ ModuleNotFoundError: No module named 'pkg_resources'
99
+ - Chore(github-ci): updates the python `--help` tests for 3.11 (and
100
+ later) [Frank Harrison]
101
+ - Chore(github-ci): test python 3.9, 3.11 and 3.12 in ci. [Frank
102
+ Harrison]
103
+
104
+ We don't bother with 3.10 because we test 3.9 and boundaries for
105
+ features are on the 3.11 version.
106
+
107
+ We don't bother with earlier than 3.9, even though 3.8 is the earliest
108
+ officially supported version.
109
+
110
+
111
+ 0.0.28 (2024-03-01)
112
+ -------------------
113
+ - Release: version 0.0.28 🚀 [Frank Harrison]
114
+ - Merge pull request #35 from lursight/fix/aliases_not_setting_options.
115
+ [Frank Harrison]
116
+
117
+ Fix/aliases not setting options
118
+ - Switches the API so 'read-only' Options are the prominent type. [Frank
119
+ Harrison]
120
+
121
+ ... and Writable are the recessive type, making it easier for API users to ensure they're using the correct type
122
+ - Uses OptionsReadOnly, stopping accidentally overwritting options by
123
+ individual jobs. [Frank Harrison]
124
+ - Adds a read-only version of the Options dict. [Frank Harrison]
125
+ - Ports Options type to an InformativeDict. [Frank Harrison]
126
+
127
+ This shows you what options are available in the options dict if you
128
+ look up a value that doesn't exist.
129
+ - Updates logging for clarity. [Frank Harrison]
130
+ - Merge pull request #34 from lursight/chore/github-actions. [Frank
131
+ Harrison]
132
+
133
+ Chore/GitHub actions
134
+ - Chore(github-ci): knocks out windows ci/cd for now. [Frank Harrison]
135
+ - Chore(github-ci): sets the windows ci to use utf-8. [Frank Harrison]
136
+ - Chore(github-ci): HACK ignore coverage in tested code. [Frank
137
+ Harrison]
138
+
139
+ It's not cler why the ci/cd thinks this isn't being hit, it is locally
140
+ and I can't figure out why it wouldn't be on the ci/cd, perhaps it's
141
+ down to the multithreaded pytest runs, but that would be
142
+ non-deterministic. This will probably need looking into at some point.
143
+ - Chore(ci-coverage): updates the _get_jobs_matching test case. [Frank
144
+ Harrison]
145
+
146
+ Making it more explicit and easier to follow
147
+ - Chore(ci-coverage): fixes the help-test false-positive. [Frank
148
+ Harrison]
149
+
150
+ We were always writing the help"
151
+ - Chore(ci-coverage): adds test for
152
+ test_run_command_basic_call_verbose_with_cwd. [Frank Harrison]
153
+ - Chore(ci-coverage): ignores un-hit 'communicate' mock function. [Frank
154
+ Harrison]
155
+ - Chore(github-ci): upload reports on failures. [Frank Harrison]
156
+ - Chore(github-ci): revert back to using the makefile for the redundant
157
+ checks. [Frank Harrison]
158
+ - Chore(github-ci): installs yarn deps for ci job. [Frank Harrison]
159
+ - Chore(github-ci): makes the log output less verbose by using --no-
160
+ spinner. [Frank Harrison]
161
+ - Chore(github-ci): moves the redundancy checks first to own job. [Frank
162
+ Harrison]
163
+ - Chore(github-ci): runs two basic checks for redunancy during ci.
164
+ [Frank Harrison]
165
+
166
+ This should help catch errors where runem has been broken and returns a
167
+ false-positive when run against itself.
168
+ - Chore(github-ci): runs runem against itself. [Frank Harrison]
169
+
170
+ This could have drawbacks later so we will think about adding some
171
+ redundancy to the ci checks.
172
+ - Merge pull request #33 from lursight/fix/mutiline_stdout. [Frank
173
+ Harrison]
174
+
175
+ fix(stdout-parsing): ensures that trailing newlines are handled
176
+ - Fix(stdout-parsing): ensures that trailing newlines are handled.
177
+ [Frank Harrison]
178
+
179
+ There will be a slight performance cost to this, hopefully not too much.
180
+
181
+
182
+ 0.0.27 (2024-02-26)
183
+ -------------------
184
+ - Release: version 0.0.27 🚀 [Frank Harrison]
7
185
  - Merge pull request #32 from lursight/feat/min-version_check. [Frank
8
186
  Harrison]
9
187
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: runem
3
- Version: 0.0.27
3
+ Version: 0.0.29
4
4
  Summary: Awesome runem created by lursight
5
5
  Home-page: https://github.com/lursight/runem/
6
6
  Author: lursight
@@ -26,6 +26,7 @@ Requires-Dist: pytest-cov==4.1.0; extra == "test"
26
26
  Requires-Dist: pytest-profiling==1.7.0; extra == "test"
27
27
  Requires-Dist: pytest-xdist==3.3.1; extra == "test"
28
28
  Requires-Dist: pytest==7.4.3; extra == "test"
29
+ Requires-Dist: setuptools; extra == "test"
29
30
  Requires-Dist: termplotlib==0.3.9; extra == "test"
30
31
  Requires-Dist: types-PyYAML==6.0.12.12; extra == "test"
31
32
  Requires-Dist: requests-mock==1.10.0; extra == "test"
@@ -400,12 +401,12 @@ runem: reports:
400
401
  runem: runem: 8.820488s
401
402
  runem: ├runem.pre-build: 0.019031s
402
403
  runem: ├runem.run-phases: 8.801317s
403
- runem: ├pre-run (total): 0.00498s
404
+ runem: ├pre-run (user-time): 0.00498s
404
405
  runem: │├pre-run.install python requirements: 2.6e-05s
405
406
  runem: │├pre-run.ls -alh runem: 0.004954s
406
- runem: ├edit (total): 0.557559s
407
+ runem: ├edit (user-time): 0.557559s
407
408
  runem: │├edit.reformat py: 0.557559s
408
- runem: ├analysis (total): 21.526145s
409
+ runem: ├analysis (user-time): 21.526145s
409
410
  runem: │├analysis.pylint py: 7.457029s
410
411
  runem: │├analysis.flake8 py: 0.693754s
411
412
  runem: │├analysis.mypy py: 1.071956s
@@ -430,12 +431,12 @@ runem: reports:
430
431
  runem [14.174612] ███████████████▋
431
432
  ├runem.pre-build [ 0.025858]
432
433
  ├runem.run-phases [14.148587] ███████████████▋
433
- ├pre-run (total) [ 0.005825]
434
+ ├pre-run (user-time) [ 0.005825]
434
435
  │├pre-run.install python requirements [ 0.000028]
435
436
  │├pre-run.ls -alh runem [ 0.005797]
436
- ├edit (total) [ 0.579153] ▋
437
+ ├edit (user-time) [ 0.579153] ▋
437
438
  │├edit.reformat py [ 0.579153] ▋
438
- ├analysis (total) [36.231034] ████████████████████████████████████████
439
+ ├analysis (user-time) [36.231034] ████████████████████████████████████████
439
440
  │├analysis.pylint py [12.738303] ██████████████▏
440
441
  │├analysis.flake8 py [ 0.798575] ▉
441
442
  │├analysis.mypy py [ 0.335984] ▍
@@ -367,12 +367,12 @@ runem: reports:
367
367
  runem: runem: 8.820488s
368
368
  runem: ├runem.pre-build: 0.019031s
369
369
  runem: ├runem.run-phases: 8.801317s
370
- runem: ├pre-run (total): 0.00498s
370
+ runem: ├pre-run (user-time): 0.00498s
371
371
  runem: │├pre-run.install python requirements: 2.6e-05s
372
372
  runem: │├pre-run.ls -alh runem: 0.004954s
373
- runem: ├edit (total): 0.557559s
373
+ runem: ├edit (user-time): 0.557559s
374
374
  runem: │├edit.reformat py: 0.557559s
375
- runem: ├analysis (total): 21.526145s
375
+ runem: ├analysis (user-time): 21.526145s
376
376
  runem: │├analysis.pylint py: 7.457029s
377
377
  runem: │├analysis.flake8 py: 0.693754s
378
378
  runem: │├analysis.mypy py: 1.071956s
@@ -397,12 +397,12 @@ runem: reports:
397
397
  runem [14.174612] ███████████████▋
398
398
  ├runem.pre-build [ 0.025858]
399
399
  ├runem.run-phases [14.148587] ███████████████▋
400
- ├pre-run (total) [ 0.005825]
400
+ ├pre-run (user-time) [ 0.005825]
401
401
  │├pre-run.install python requirements [ 0.000028]
402
402
  │├pre-run.ls -alh runem [ 0.005797]
403
- ├edit (total) [ 0.579153] ▋
403
+ ├edit (user-time) [ 0.579153] ▋
404
404
  │├edit.reformat py [ 0.579153] ▋
405
- ├analysis (total) [36.231034] ████████████████████████████████████████
405
+ ├analysis (user-time) [36.231034] ████████████████████████████████████████
406
406
  │├analysis.pylint py [12.738303] ██████████████▏
407
407
  │├analysis.flake8 py [ 0.798575] ▉
408
408
  │├analysis.mypy py [ 0.335984] ▍
@@ -0,0 +1 @@
1
+ 0.0.29
@@ -1,21 +1,22 @@
1
1
  import argparse
2
2
 
3
3
  from runem.config_metadata import ConfigMetadata
4
- from runem.types import Options
4
+ from runem.informative_dict import InformativeDict
5
+ from runem.types import OptionsWritable
5
6
 
6
7
 
7
8
  def initialise_options(
8
9
  config_metadata: ConfigMetadata,
9
10
  args: argparse.Namespace,
10
- ) -> Options:
11
+ ) -> OptionsWritable:
11
12
  """Initialises and returns the set of options to use for this run.
12
13
 
13
14
  Returns the options dictionary
14
15
  """
15
16
 
16
- options: Options = {
17
- option["name"]: option["default"] for option in config_metadata.options_config
18
- }
17
+ options: OptionsWritable = InformativeDict(
18
+ {option["name"]: option["default"] for option in config_metadata.options_config}
19
+ )
19
20
  if config_metadata.options_config and args.overrides_on: # pragma: no branch
20
21
  for option_name in args.overrides_on: # pragma: no branch
21
22
  options[option_name] = True
@@ -5,9 +5,10 @@ import sys
5
5
  import typing
6
6
 
7
7
  from runem.config_metadata import ConfigMetadata
8
+ from runem.informative_dict import InformativeDict
8
9
  from runem.log import log
9
10
  from runem.runem_version import get_runem_version
10
- from runem.types import JobNames, OptionConfig, Options
11
+ from runem.types import JobNames, OptionConfig, OptionsWritable
11
12
  from runem.utils import printable_set
12
13
 
13
14
 
@@ -175,7 +176,7 @@ def parse_args(
175
176
  # cleanly exit
176
177
  sys.exit(0)
177
178
 
178
- options: Options = initialise_options(config_metadata, args)
179
+ options: OptionsWritable = initialise_options(config_metadata, args)
179
180
 
180
181
  if not _validate_filters(config_metadata, args):
181
182
  sys.exit(1)
@@ -245,15 +246,15 @@ def _validate_filters(
245
246
  def initialise_options(
246
247
  config_metadata: ConfigMetadata,
247
248
  args: argparse.Namespace,
248
- ) -> Options:
249
+ ) -> OptionsWritable:
249
250
  """Initialises and returns the set of options to use for this run.
250
251
 
251
252
  Returns the options dictionary
252
253
  """
253
254
 
254
- options: Options = {
255
- option["name"]: option["default"] for option in config_metadata.options_config
256
- }
255
+ options: OptionsWritable = InformativeDict(
256
+ {option["name"]: option["default"] for option in config_metadata.options_config}
257
+ )
257
258
  if config_metadata.options_config and args.overrides_on: # pragma: no branch
258
259
  for option_name in args.overrides_on: # pragma: no branch
259
260
  options[option_name] = True
@@ -1,12 +1,13 @@
1
1
  import argparse
2
2
  import pathlib
3
3
 
4
+ from runem.informative_dict import InformativeDict
4
5
  from runem.types import (
5
6
  JobNames,
6
7
  JobPhases,
7
8
  JobTags,
8
9
  OptionConfigs,
9
- Options,
10
+ OptionsWritable,
10
11
  OrderedPhases,
11
12
  PhaseGroupedJobs,
12
13
  TagFileFilters,
@@ -24,7 +25,7 @@ class ConfigMetadata:
24
25
  all_job_phases: JobPhases # the set of job-phases (should be subset of 'phases')
25
26
  all_job_tags: JobTags # the set of job-tags (used for filtering)
26
27
 
27
- options: Options # the final configured options to pass to jobs
28
+ options: OptionsWritable # the final configured options to pass to jobs
28
29
 
29
30
  args: argparse.Namespace # the raw cli args, probably missing information
30
31
  jobs_to_run: JobNames # superset of job-name candidates to run, from cli+config
@@ -52,7 +53,7 @@ class ConfigMetadata:
52
53
  self.all_job_phases = all_job_phases
53
54
  self.all_job_tags = all_job_tags
54
55
 
55
- self.options = {} # will be defined after cli argument parsing
56
+ self.options = InformativeDict() # shows useful errors on bad-option lookups
56
57
 
57
58
  self.args = (
58
59
  argparse.Namespace()
@@ -69,7 +70,7 @@ class ConfigMetadata:
69
70
  phases_to_run: JobPhases,
70
71
  tags_to_run: JobTags,
71
72
  tags_to_avoid: JobTags,
72
- options: Options,
73
+ options: OptionsWritable,
73
74
  ) -> None:
74
75
  self.options = options
75
76
  self.args = args
@@ -0,0 +1,42 @@
1
+ import typing
2
+
3
+ # Define type variables for key and value to be used in the custom dictionary
4
+ K = typing.TypeVar("K")
5
+ V = typing.TypeVar("V")
6
+
7
+
8
+ class InformativeDict(typing.Dict[K, V], typing.Generic[K, V]):
9
+ """A dictionary type that prints out the available keys."""
10
+
11
+ def __getitem__(self, key: K) -> V:
12
+ """Attempt to retrieve an item, raising a detailed exception if the key is not
13
+ found."""
14
+ try:
15
+ return super().__getitem__(key)
16
+ except KeyError:
17
+ available_keys: typing.Iterable[str] = (str(k) for k in self.keys())
18
+ raise KeyError(
19
+ f"Key '{key}' not found. Available keys: {', '.join(available_keys)}"
20
+ ) from None
21
+
22
+
23
+ class ReadOnlyInformativeDict(InformativeDict[K, V], typing.Generic[K, V]):
24
+ """A read-only variant of the above."""
25
+
26
+ def __setitem__(self, key: K, value: V) -> None:
27
+ raise NotImplementedError("This dictionary is read-only")
28
+
29
+ def __delitem__(self, key: K) -> None:
30
+ raise NotImplementedError("This dictionary is read-only")
31
+
32
+ def pop(self, *args: typing.Any, **kwargs: typing.Any) -> V:
33
+ raise NotImplementedError("This dictionary is read-only")
34
+
35
+ def popitem(self) -> typing.Tuple[K, V]:
36
+ raise NotImplementedError("This dictionary is read-only")
37
+
38
+ def clear(self) -> None:
39
+ raise NotImplementedError("This dictionary is read-only")
40
+
41
+ def update(self, *args: typing.Any, **kwargs: typing.Any) -> None:
42
+ raise NotImplementedError("This dictionary is read-only")
@@ -7,24 +7,34 @@ from datetime import timedelta
7
7
  from timeit import default_timer as timer
8
8
 
9
9
  from runem.config_metadata import ConfigMetadata
10
+ from runem.informative_dict import ReadOnlyInformativeDict
10
11
  from runem.job import Job
11
12
  from runem.job_wrapper import get_job_wrapper
12
13
  from runem.log import log
13
- from runem.types import FilePathListLookup, JobConfig, JobFunction, JobReturn, JobTags
14
+ from runem.types import (
15
+ FilePathListLookup,
16
+ JobConfig,
17
+ JobFunction,
18
+ JobReturn,
19
+ JobTags,
20
+ JobTiming,
21
+ TimingEntries,
22
+ TimingEntry,
23
+ )
14
24
 
15
25
 
16
26
  def job_execute_inner(
17
27
  job_config: JobConfig,
18
28
  config_metadata: ConfigMetadata,
19
29
  file_lists: FilePathListLookup,
20
- ) -> typing.Tuple[typing.Tuple[str, timedelta], JobReturn]:
30
+ ) -> typing.Tuple[JobTiming, JobReturn]:
21
31
  """Wrapper for running a job inside a sub-process.
22
32
 
23
33
  Returns the time information and any reports the job generated
24
34
  """
25
35
  label = Job.get_job_name(job_config)
26
36
  if config_metadata.args.verbose:
27
- log(f"START: {label}")
37
+ log(f"START: '{label}'")
28
38
  root_path: pathlib.Path = config_metadata.cfg_filepath.parent
29
39
  function: JobFunction
30
40
  job_tags: typing.Optional[JobTags] = Job.get_job_tags(job_config)
@@ -37,7 +47,19 @@ def job_execute_inner(
37
47
  if not file_list:
38
48
  # no files to work on
39
49
  log(f"WARNING: skipping job '{label}', no files for job")
40
- return (f"{label}: no files!", timedelta(0)), None
50
+ return {
51
+ "job": (f"{label}: no files!", timedelta(0)),
52
+ "commands": [],
53
+ }, None
54
+
55
+ sub_command_timings: TimingEntries = []
56
+
57
+ def _record_sub_job_time(label: str, timing: timedelta) -> None:
58
+ """Record timing information for sub-commands/tasks, atomically.
59
+
60
+ For example inside of run_command() calls
61
+ """
62
+ sub_command_timings.append((label, timing))
41
63
 
42
64
  if (
43
65
  "ctx" in job_config
@@ -53,29 +75,38 @@ def job_execute_inner(
53
75
  start = timer()
54
76
  func_signature = inspect.signature(function)
55
77
  if config_metadata.args.verbose:
56
- log(f"job: running {Job.get_job_name(job_config)}")
78
+ log(f"job: running: '{Job.get_job_name(job_config)}'")
57
79
  reports: JobReturn
58
- if "args" in func_signature.parameters:
59
- reports = function( # type: ignore # FIXME: which function do we have?
60
- config_metadata.args, config_metadata.options, file_list
61
- )
62
- else:
63
- reports = function(
64
- options=config_metadata.options, # type: ignore
65
- file_list=file_list,
66
- procs=config_metadata.args.procs,
67
- root_path=root_path,
68
- verbose=config_metadata.args.verbose,
69
- # unpack useful data points from the job_config
70
- label=Job.get_job_name(job_config),
71
- job=job_config,
72
- )
80
+ try:
81
+ if "args" in func_signature.parameters:
82
+ reports = function( # type: ignore # FIXME: which function do we have?
83
+ config_metadata.args, config_metadata.options, file_list
84
+ )
85
+ else:
86
+ reports = function(
87
+ options=ReadOnlyInformativeDict(config_metadata.options), # type: ignore
88
+ file_list=file_list,
89
+ procs=config_metadata.args.procs,
90
+ root_path=root_path,
91
+ verbose=config_metadata.args.verbose,
92
+ # unpack useful data points from the job_config
93
+ label=Job.get_job_name(job_config),
94
+ job=job_config,
95
+ record_sub_job_time=_record_sub_job_time,
96
+ )
97
+ except BaseException: # pylint: disable=broad-exception-caught
98
+ # log that we hit an error on this job and re-raise
99
+ log(decorate=False)
100
+ log(f"job: ERROR: job '{Job.get_job_name(job_config)}' failed to complete!")
101
+ # re-raise
102
+ raise
103
+
73
104
  end = timer()
74
105
  time_taken: timedelta = timedelta(seconds=end - start)
75
106
  if config_metadata.args.verbose:
76
- log(f"DONE: {label}: {time_taken}")
77
- timing_data = (label, time_taken)
78
- return (timing_data, reports)
107
+ log(f"job: DONE: '{label}': {time_taken}")
108
+ this_job_timing_data: TimingEntry = (label, time_taken)
109
+ return ({"job": this_job_timing_data, "commands": sub_command_timings}, reports)
79
110
 
80
111
 
81
112
  def job_execute(
@@ -83,7 +114,7 @@ def job_execute(
83
114
  running_jobs: typing.Dict[str, str],
84
115
  config_metadata: ConfigMetadata,
85
116
  file_lists: FilePathListLookup,
86
- ) -> typing.Tuple[typing.Tuple[str, timedelta], JobReturn]:
117
+ ) -> typing.Tuple[JobTiming, JobReturn]:
87
118
  """Thin-wrapper around job_execute_inner needed for mocking in tests.
88
119
 
89
120
  Needed for faster tests.
@@ -65,6 +65,10 @@ def _get_jobs_matching(
65
65
  filtered_jobs: PhaseGroupedJobs,
66
66
  verbose: bool,
67
67
  ) -> None:
68
+ """Via filtered_jobs, filters 'jobs' that match the given phase and and tags.
69
+
70
+ Warns if the job-name isn't found in list of valid job-names.
71
+ """
68
72
  phase_jobs: typing.List[JobConfig] = jobs[phase]
69
73
 
70
74
  job: JobConfig
@@ -74,7 +78,10 @@ def _get_jobs_matching(
74
78
 
75
79
  job_name: str = Job.get_job_name(job)
76
80
  if job_name not in job_names:
77
- if verbose:
81
+ # test test_get_jobs_matching_when_job_not_in_valid_job_names should
82
+ # cover the follow in Ci but does not for some reason I don't have
83
+ # time to look in to. /FH
84
+ if verbose: # pragma: FIXME: add code coverage
78
85
  log(
79
86
  (
80
87
  f"not running job '{job_name}' because it isn't in the "
@@ -118,7 +125,10 @@ def filter_jobs( # noqa: C901
118
125
  filtered_jobs: PhaseGroupedJobs = defaultdict(list)
119
126
  for phase in config_metadata.phases:
120
127
  if phase not in phases_to_run:
121
- if verbose:
128
+ # test test_get_jobs_matching_when_job_not_in_valid_job_names should
129
+ # cover the follow in Ci but does not for some reason I don't have
130
+ # time to look in to. /FH
131
+ if verbose: # pragma: FIXME: add code coverage
122
132
  log(f"skipping phase '{phase}'")
123
133
  continue
124
134
  _get_jobs_matching(
@@ -131,7 +141,10 @@ def filter_jobs( # noqa: C901
131
141
  verbose=verbose,
132
142
  )
133
143
  if len(filtered_jobs[phase]) == 0:
134
- if verbose:
144
+ # test test_get_jobs_matching_when_job_not_in_valid_job_names should
145
+ # cover the follow in Ci but does not for some reason I don't have
146
+ # time to look in to. /FH
147
+ if verbose: # pragma: FIXME: add code coverage
135
148
  log(f"No jobs for phase '{phase}' tags {printable_set(tags_to_run)}")
136
149
  continue
137
150