runem 0.4.0__py3-none-any.whl → 0.6.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- runem/VERSION +1 -1
- runem/blocking_print.py +10 -2
- runem/cli/initialise_options.py +26 -0
- runem/command_line.py +23 -1
- runem/config_parse.py +21 -7
- runem/job_execute.py +3 -2
- runem/log.py +23 -4
- runem/report.py +4 -1
- runem/run_command.py +62 -26
- runem/runem.py +46 -60
- runem/types/__init__.py +2 -1
- runem/utils.py +12 -0
- runem-0.6.0.dist-info/METADATA +161 -0
- runem-0.6.0.dist-info/RECORD +52 -0
- {runem-0.4.0.dist-info → runem-0.6.0.dist-info}/WHEEL +1 -1
- {runem-0.4.0.dist-info → runem-0.6.0.dist-info}/top_level.txt +1 -0
- scripts/test_hooks/__init__.py +0 -0
- scripts/test_hooks/json_validators.py +32 -0
- scripts/test_hooks/py.py +292 -0
- scripts/test_hooks/py.typed +0 -0
- scripts/test_hooks/runem_hooks.py +19 -0
- scripts/test_hooks/yarn.py +48 -0
- tests/cli/test_initialise_options.py +105 -0
- tests/data/help_output.3.10.txt +55 -0
- tests/data/help_output.3.11.txt +55 -0
- runem-0.4.0.dist-info/METADATA +0 -155
- runem-0.4.0.dist-info/RECORD +0 -42
- {runem-0.4.0.dist-info → runem-0.6.0.dist-info}/LICENSE +0 -0
- {runem-0.4.0.dist-info → runem-0.6.0.dist-info}/entry_points.txt +0 -0
runem/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
0.
|
1
|
+
0.6.0
|
runem/blocking_print.py
CHANGED
@@ -13,7 +13,14 @@ def _reset_console() -> Console:
|
|
13
13
|
|
14
14
|
RICH_CONSOLE = Console(
|
15
15
|
log_path=False, # Do NOT print the source path.
|
16
|
-
markup
|
16
|
+
# We allow markup here, BUT stdout/stderr from other procs should have
|
17
|
+
# `escape()` called on them so they don't error here.
|
18
|
+
# This means 'rich' effects/colors can be judiciously applied:
|
19
|
+
# e.g. `[blink]Don't Panic![/blink]`.
|
20
|
+
markup=True,
|
21
|
+
# `highlight` is what colourises string and number in print() calls.
|
22
|
+
# We do not want this to be auto-magic.
|
23
|
+
highlight=False,
|
17
24
|
)
|
18
25
|
return RICH_CONSOLE
|
19
26
|
|
@@ -31,7 +38,8 @@ def _reset_console_for_tests() -> None:
|
|
31
38
|
RICH_CONSOLE = Console(
|
32
39
|
log_path=False, # Do NOT print the source path.
|
33
40
|
log_time=False, # Do not prefix with log time e.g. `[time] log message`.
|
34
|
-
markup=
|
41
|
+
markup=True, # Allow some markup e.g. `[blink]Don't Panic![/blink]`.
|
42
|
+
highlight=False,
|
35
43
|
width=999, # A very wide width.
|
36
44
|
)
|
37
45
|
|
@@ -0,0 +1,26 @@
|
|
1
|
+
import argparse
|
2
|
+
|
3
|
+
from runem.config_metadata import ConfigMetadata
|
4
|
+
from runem.informative_dict import InformativeDict
|
5
|
+
from runem.types.options import OptionsWritable
|
6
|
+
|
7
|
+
|
8
|
+
def initialise_options(
|
9
|
+
config_metadata: ConfigMetadata,
|
10
|
+
args: argparse.Namespace,
|
11
|
+
) -> OptionsWritable:
|
12
|
+
"""Initialises and returns the set of options to use for this run.
|
13
|
+
|
14
|
+
Returns the options dictionary
|
15
|
+
"""
|
16
|
+
|
17
|
+
options: OptionsWritable = InformativeDict(
|
18
|
+
{option["name"]: option["default"] for option in config_metadata.options_config}
|
19
|
+
)
|
20
|
+
if config_metadata.options_config and args.overrides_on: # pragma: no branch
|
21
|
+
for option_name in args.overrides_on: # pragma: no branch
|
22
|
+
options[option_name] = True
|
23
|
+
if config_metadata.options_config and args.overrides_off: # pragma: no branch
|
24
|
+
for option_name in args.overrides_off:
|
25
|
+
options[option_name] = False
|
26
|
+
return options
|
runem/command_line.py
CHANGED
@@ -53,6 +53,14 @@ def _get_argparse_help_formatter() -> typing.Any:
|
|
53
53
|
return argparse.HelpFormatter
|
54
54
|
|
55
55
|
|
56
|
+
def error_on_log_logic(verbose: bool, silent: bool) -> None:
|
57
|
+
"""Simply errors if we get logical inconsistencies in the logging-logic."""
|
58
|
+
if verbose and silent:
|
59
|
+
log("cannot parse '--verbose' and '--silent'")
|
60
|
+
# error exit
|
61
|
+
sys.exit(1)
|
62
|
+
|
63
|
+
|
56
64
|
def parse_args(
|
57
65
|
config_metadata: ConfigMetadata, argv: typing.List[str]
|
58
66
|
) -> ConfigMetadata:
|
@@ -130,7 +138,9 @@ def parse_args(
|
|
130
138
|
nargs="+",
|
131
139
|
default=config_metadata.all_job_tags,
|
132
140
|
help=(
|
133
|
-
|
141
|
+
# TODO: clarify the logic by which we add/remove jobs based on tags
|
142
|
+
# e.g. exclusive-in, union, x-or etc.
|
143
|
+
"Only run jobs with the given tags. "
|
134
144
|
f"Defaults to '{sorted(config_metadata.all_job_tags)}'."
|
135
145
|
),
|
136
146
|
required=False,
|
@@ -238,6 +248,16 @@ def parse_args(
|
|
238
248
|
required=False,
|
239
249
|
)
|
240
250
|
|
251
|
+
parser.add_argument(
|
252
|
+
"--silent",
|
253
|
+
"-s",
|
254
|
+
dest="silent",
|
255
|
+
action=argparse.BooleanOptionalAction,
|
256
|
+
default=False,
|
257
|
+
help=("Whether to show warning messages or not. "),
|
258
|
+
required=False,
|
259
|
+
)
|
260
|
+
|
241
261
|
parser.add_argument(
|
242
262
|
"--spinner",
|
243
263
|
dest="show_spinner",
|
@@ -271,6 +291,8 @@ def parse_args(
|
|
271
291
|
|
272
292
|
args = parser.parse_args(argv[1:])
|
273
293
|
|
294
|
+
error_on_log_logic(args.verbose, args.silent)
|
295
|
+
|
274
296
|
if args.show_root_path_and_exit:
|
275
297
|
log(str(config_metadata.cfg_filepath.parent), decorate=False)
|
276
298
|
# cleanly exit
|
runem/config_parse.py
CHANGED
@@ -149,6 +149,7 @@ def parse_job_config(
|
|
149
149
|
in_out_job_names: JobNames,
|
150
150
|
in_out_phases: JobPhases,
|
151
151
|
phase_order: OrderedPhases,
|
152
|
+
silent: bool = False,
|
152
153
|
) -> None:
|
153
154
|
"""Parses and validates a job-entry read in from disk.
|
154
155
|
|
@@ -208,6 +209,7 @@ def parse_job_config(
|
|
208
209
|
in_out_job_names,
|
209
210
|
in_out_phases,
|
210
211
|
phase_order,
|
212
|
+
warn_missing_phase=(not silent),
|
211
213
|
)
|
212
214
|
except KeyError as err:
|
213
215
|
raise ValueError(
|
@@ -215,8 +217,11 @@ def parse_job_config(
|
|
215
217
|
) from err
|
216
218
|
|
217
219
|
|
218
|
-
def parse_config(
|
219
|
-
config: Config,
|
220
|
+
def parse_config( # noqa: C901
|
221
|
+
config: Config,
|
222
|
+
cfg_filepath: pathlib.Path,
|
223
|
+
silent: bool = False,
|
224
|
+
hooks_only: bool = False,
|
220
225
|
) -> typing.Tuple[
|
221
226
|
Hooks, # hooks:
|
222
227
|
OrderedPhases, # phase_order:
|
@@ -282,7 +287,10 @@ def parse_config(
|
|
282
287
|
|
283
288
|
if not phase_order:
|
284
289
|
if not hooks_only:
|
285
|
-
|
290
|
+
if silent: # pragma: no cover
|
291
|
+
pass
|
292
|
+
else:
|
293
|
+
warn("phase ordering not configured! Runs will be non-deterministic!")
|
286
294
|
phase_order = tuple(job_phases)
|
287
295
|
|
288
296
|
# now parse out the job_configs
|
@@ -301,6 +309,7 @@ def parse_config(
|
|
301
309
|
in_out_job_names=job_names,
|
302
310
|
in_out_phases=job_phases,
|
303
311
|
phase_order=phase_order,
|
312
|
+
silent=silent,
|
304
313
|
)
|
305
314
|
return (
|
306
315
|
hooks,
|
@@ -341,7 +350,9 @@ def generate_config(
|
|
341
350
|
|
342
351
|
|
343
352
|
def _load_user_hooks_from_config(
|
344
|
-
user_config: Config,
|
353
|
+
user_config: Config,
|
354
|
+
cfg_filepath: pathlib.Path,
|
355
|
+
silent: bool,
|
345
356
|
) -> Hooks:
|
346
357
|
hooks: Hooks
|
347
358
|
(
|
@@ -353,7 +364,7 @@ def _load_user_hooks_from_config(
|
|
353
364
|
_,
|
354
365
|
_,
|
355
366
|
_,
|
356
|
-
) = parse_config(user_config, cfg_filepath, hooks_only=True)
|
367
|
+
) = parse_config(user_config, cfg_filepath, silent, hooks_only=True)
|
357
368
|
return hooks
|
358
369
|
|
359
370
|
|
@@ -361,6 +372,7 @@ def load_config_metadata(
|
|
361
372
|
config: Config,
|
362
373
|
cfg_filepath: pathlib.Path,
|
363
374
|
user_configs: typing.List[typing.Tuple[Config, pathlib.Path]],
|
375
|
+
silent: bool = False,
|
364
376
|
verbose: bool = False,
|
365
377
|
) -> ConfigMetadata:
|
366
378
|
hooks: Hooks
|
@@ -380,12 +392,14 @@ def load_config_metadata(
|
|
380
392
|
job_names,
|
381
393
|
job_phases,
|
382
394
|
tags,
|
383
|
-
) = parse_config(config, cfg_filepath)
|
395
|
+
) = parse_config(config, cfg_filepath, silent)
|
384
396
|
|
385
397
|
user_config: Config
|
386
398
|
user_config_path: pathlib.Path
|
387
399
|
for user_config, user_config_path in user_configs:
|
388
|
-
user_hooks: Hooks = _load_user_hooks_from_config(
|
400
|
+
user_hooks: Hooks = _load_user_hooks_from_config(
|
401
|
+
user_config, user_config_path, silent
|
402
|
+
)
|
389
403
|
if user_hooks:
|
390
404
|
if verbose:
|
391
405
|
log(f"hooks: loading user hooks from '{str(user_config_path)}'")
|
runem/job_execute.py
CHANGED
@@ -11,7 +11,7 @@ from runem.config_metadata import ConfigMetadata
|
|
11
11
|
from runem.informative_dict import ReadOnlyInformativeDict
|
12
12
|
from runem.job import Job
|
13
13
|
from runem.job_wrapper import get_job_wrapper
|
14
|
-
from runem.log import error, log
|
14
|
+
from runem.log import error, log, warn
|
15
15
|
from runem.types.common import FilePathList, JobTags
|
16
16
|
from runem.types.filters import FilePathListLookup
|
17
17
|
from runem.types.runem_config import JobConfig
|
@@ -50,7 +50,8 @@ def job_execute_inner(
|
|
50
50
|
|
51
51
|
if not file_list:
|
52
52
|
# no files to work on
|
53
|
-
|
53
|
+
if not config_metadata.args.silent:
|
54
|
+
warn(f"skipping job '{label}', no files for job")
|
54
55
|
return {
|
55
56
|
"job": (f"{label}: no files!", timedelta(0)),
|
56
57
|
"commands": [],
|
runem/log.py
CHANGED
@@ -3,13 +3,32 @@ import typing
|
|
3
3
|
from runem.blocking_print import blocking_print
|
4
4
|
|
5
5
|
|
6
|
-
def log(
|
6
|
+
def log(
|
7
|
+
msg: str = "",
|
8
|
+
decorate: bool = True,
|
9
|
+
end: typing.Optional[str] = None,
|
10
|
+
) -> None:
|
7
11
|
"""Thin wrapper around 'print', change the 'msg' & handles system-errors.
|
8
12
|
|
9
13
|
One way we change it is to decorate the output with 'runem'
|
14
|
+
|
15
|
+
Parameters:
|
16
|
+
msg: str - the message to log out. Any `rich` markup will be escaped
|
17
|
+
and not applied.
|
18
|
+
decorate: str - whether to add runem-specific prefix text. We do this
|
19
|
+
to identify text that comes from the app vs text that
|
20
|
+
comes from hooks or other third-parties.
|
21
|
+
end: Optional[str] - same as the end option used by `print()` and
|
22
|
+
`rich`
|
23
|
+
Returns:
|
24
|
+
Nothing
|
10
25
|
"""
|
26
|
+
# Remove any markup as it will probably error, if unsanitised.
|
27
|
+
# msg = escape(msg)
|
28
|
+
|
11
29
|
if decorate:
|
12
|
-
|
30
|
+
# Make it clear that the message comes from `runem` internals.
|
31
|
+
msg = f"[light_slate_grey]runem[/light_slate_grey]: {msg}"
|
13
32
|
|
14
33
|
# print in a blocking manner, waiting for system resources to free up if a
|
15
34
|
# runem job is contending on stdout or similar.
|
@@ -17,8 +36,8 @@ def log(msg: str = "", decorate: bool = True, end: typing.Optional[str] = None)
|
|
17
36
|
|
18
37
|
|
19
38
|
def warn(msg: str) -> None:
|
20
|
-
log(f"WARNING: {msg}")
|
39
|
+
log(f"[yellow]WARNING[/yellow]: {msg}")
|
21
40
|
|
22
41
|
|
23
42
|
def error(msg: str) -> None:
|
24
|
-
log(f"ERROR: {msg}")
|
43
|
+
log(f"[red]ERROR[/red]: {msg}")
|
runem/report.py
CHANGED
@@ -211,7 +211,10 @@ def _print_reports_by_phase(
|
|
211
211
|
for job_report_url_info in report_urls:
|
212
212
|
if not job_report_url_info:
|
213
213
|
continue
|
214
|
-
log(
|
214
|
+
log(
|
215
|
+
f"report: [blue]{str(job_report_url_info[0])}[/blue]: "
|
216
|
+
f"{str(job_report_url_info[1])}"
|
217
|
+
)
|
215
218
|
|
216
219
|
|
217
220
|
def report_on_run(
|
runem/run_command.py
CHANGED
@@ -7,17 +7,33 @@ from subprocess import STDOUT as SUBPROCESS_STDOUT
|
|
7
7
|
from subprocess import Popen
|
8
8
|
from timeit import default_timer as timer
|
9
9
|
|
10
|
+
from rich.markup import escape
|
11
|
+
|
10
12
|
from runem.log import log
|
11
13
|
|
12
14
|
TERMINAL_WIDTH = 86
|
13
15
|
|
14
16
|
|
15
|
-
class
|
16
|
-
|
17
|
+
class RunemJobError(RuntimeError):
|
18
|
+
"""An exception type that stores the stdout/stderr.
|
19
|
+
|
20
|
+
Designed so that we do not print the full stdout via the exception stack, instead,
|
21
|
+
allows an opportunity to parse the markup in it.
|
22
|
+
"""
|
23
|
+
|
24
|
+
def __init__(self, friendly_message: str, stdout: str):
|
25
|
+
self.stdout = stdout
|
26
|
+
super().__init__(friendly_message)
|
17
27
|
|
18
28
|
|
19
|
-
class
|
20
|
-
|
29
|
+
class RunCommandBadExitCode(RunemJobError):
|
30
|
+
def __init__(self, stdout: str):
|
31
|
+
super().__init__(friendly_message="Bad exit-code", stdout=stdout)
|
32
|
+
|
33
|
+
|
34
|
+
class RunCommandUnhandledError(RunemJobError):
|
35
|
+
def __init__(self, stdout: str):
|
36
|
+
super().__init__(friendly_message="Unhandled job error", stdout=stdout)
|
21
37
|
|
22
38
|
|
23
39
|
# A function type for recording timing information.
|
@@ -27,25 +43,25 @@ RecordSubJobTimeType = typing.Callable[[str, timedelta], None]
|
|
27
43
|
def parse_stdout(stdout: str, prefix: str) -> str:
|
28
44
|
"""Prefixes each line of the output with a given label, except trailing new
|
29
45
|
lines."""
|
46
|
+
|
30
47
|
# Edge case: Return the prefix immediately for an empty string
|
31
48
|
if not stdout:
|
32
49
|
return prefix
|
33
50
|
|
34
|
-
#
|
35
|
-
|
51
|
+
# Stop errors in `rich` by parsing out anything that might look like
|
52
|
+
# rich-markup.
|
53
|
+
stdout = escape(stdout)
|
54
|
+
|
55
|
+
# Split stdout into lines
|
36
56
|
lines = stdout.split("\n")
|
37
57
|
|
38
58
|
# Apply prefix to all lines except the last if it's empty (due to a trailing newline)
|
39
|
-
modified_lines = [f"{prefix}{line}" for line in lines[:-1]] + (
|
40
|
-
[lines[-1]]
|
41
|
-
if lines[-1] == "" and ends_with_newline
|
42
|
-
else [f"{prefix}{lines[-1]}"]
|
59
|
+
modified_lines = [f"{prefix}{escape(line)}" for line in lines[:-1]] + (
|
60
|
+
[f"{prefix}{escape(lines[-1])}"]
|
43
61
|
)
|
44
62
|
|
45
63
|
# Join the lines back together, appropriately handling the final newline
|
46
|
-
modified_stdout = "\n".join(modified_lines)
|
47
|
-
# if ends_with_newline:
|
48
|
-
# modified_stdout += "\n"
|
64
|
+
modified_stdout: str = "\n".join(modified_lines)
|
49
65
|
|
50
66
|
return modified_stdout
|
51
67
|
|
@@ -69,24 +85,34 @@ def _log_command_execution(
|
|
69
85
|
label: str,
|
70
86
|
env_overrides: typing.Optional[typing.Dict[str, str]],
|
71
87
|
valid_exit_ids: typing.Optional[typing.Tuple[int, ...]],
|
88
|
+
decorate_logs: bool,
|
72
89
|
verbose: bool,
|
73
90
|
cwd: typing.Optional[pathlib.Path] = None,
|
74
91
|
) -> None:
|
75
92
|
"""Logs out useful debug information on '--verbose'."""
|
76
93
|
if verbose:
|
77
|
-
log(
|
94
|
+
log(
|
95
|
+
f"running: start: [blue]{label}[/blue]: [yellow]{cmd_string}[yellow]",
|
96
|
+
decorate=decorate_logs,
|
97
|
+
)
|
78
98
|
if valid_exit_ids is not None:
|
79
99
|
valid_exit_strs = ",".join(str(exit_code) for exit_code in valid_exit_ids)
|
80
|
-
log(
|
100
|
+
log(
|
101
|
+
f"\tallowed return ids are: [green]{valid_exit_strs}[/green]",
|
102
|
+
decorate=decorate_logs,
|
103
|
+
)
|
81
104
|
|
82
105
|
if env_overrides:
|
83
106
|
env_overrides_as_string = " ".join(
|
84
107
|
[f"{key}='{value}'" for key, value in env_overrides.items()]
|
85
108
|
)
|
86
|
-
log(
|
109
|
+
log(
|
110
|
+
f"ENV OVERRIDES: [yellow]{env_overrides_as_string} {cmd_string}[/yellow]",
|
111
|
+
decorate=decorate_logs,
|
112
|
+
)
|
87
113
|
|
88
114
|
if cwd:
|
89
|
-
log(f"cwd: {str(cwd)}")
|
115
|
+
log(f"cwd: {str(cwd)}", decorate=decorate_logs)
|
90
116
|
|
91
117
|
|
92
118
|
def run_command( # noqa: C901
|
@@ -98,6 +124,7 @@ def run_command( # noqa: C901
|
|
98
124
|
valid_exit_ids: typing.Optional[typing.Tuple[int, ...]] = None,
|
99
125
|
cwd: typing.Optional[pathlib.Path] = None,
|
100
126
|
record_sub_job_time: typing.Optional[RecordSubJobTimeType] = None,
|
127
|
+
decorate_logs: bool = True,
|
101
128
|
**kwargs: typing.Any,
|
102
129
|
) -> str:
|
103
130
|
"""Runs the given command, returning stdout or throwing on any error."""
|
@@ -115,6 +142,7 @@ def run_command( # noqa: C901
|
|
115
142
|
label,
|
116
143
|
env_overrides,
|
117
144
|
valid_exit_ids,
|
145
|
+
decorate_logs,
|
118
146
|
verbose,
|
119
147
|
cwd,
|
120
148
|
)
|
@@ -143,7 +171,12 @@ def run_command( # noqa: C901
|
|
143
171
|
stdout += line
|
144
172
|
if verbose:
|
145
173
|
# print each line of output, assuming that each has a newline
|
146
|
-
log(
|
174
|
+
log(
|
175
|
+
parse_stdout(
|
176
|
+
line, prefix=f"[green]| [/green][blue]{label}[/blue]: "
|
177
|
+
),
|
178
|
+
decorate=False,
|
179
|
+
)
|
147
180
|
|
148
181
|
# Wait for the subprocess to finish and get the exit code
|
149
182
|
process.wait()
|
@@ -154,15 +187,15 @@ def run_command( # noqa: C901
|
|
154
187
|
)
|
155
188
|
raise RunCommandBadExitCode(
|
156
189
|
(
|
157
|
-
f"non-zero exit {process.returncode} (allowed are "
|
158
|
-
f"{valid_exit_strs}) from {cmd_string}"
|
190
|
+
f"non-zero exit [red]{process.returncode}[/red] (allowed are "
|
191
|
+
f"[green]{valid_exit_strs}[/green]) from {cmd_string}"
|
159
192
|
)
|
160
193
|
)
|
161
194
|
except BaseException as err:
|
162
195
|
if ignore_fails:
|
163
196
|
return ""
|
164
197
|
parsed_stdout: str = (
|
165
|
-
parse_stdout(stdout, prefix=
|
198
|
+
parse_stdout(stdout, prefix="[red]| [/red]") if process else ""
|
166
199
|
)
|
167
200
|
env_overrides_as_string = ""
|
168
201
|
if env_overrides:
|
@@ -171,11 +204,11 @@ def run_command( # noqa: C901
|
|
171
204
|
)
|
172
205
|
env_overrides_as_string = f"{env_overrides_as_string} "
|
173
206
|
error_string = (
|
174
|
-
f"runem:
|
175
|
-
f"\n\t{env_overrides_as_string}{cmd_string}"
|
176
|
-
f"\
|
207
|
+
f"runem: [red bold]FATAL[/red bold]: command failed: [blue]{label}[/blue]"
|
208
|
+
f"\n\t[yellow]{env_overrides_as_string}{cmd_string}[/yellow]"
|
209
|
+
f"\n[red underline]| ERROR[/red underline]"
|
177
210
|
f"\n{str(parsed_stdout)}"
|
178
|
-
f"\
|
211
|
+
f"\n[red underline]| ERROR END[/red underline]"
|
179
212
|
)
|
180
213
|
|
181
214
|
if isinstance(err, RunCommandBadExitCode):
|
@@ -184,7 +217,10 @@ def run_command( # noqa: C901
|
|
184
217
|
raise RunCommandUnhandledError(error_string) from err
|
185
218
|
|
186
219
|
if verbose:
|
187
|
-
log(
|
220
|
+
log(
|
221
|
+
f"running: done: [blue]{label}[/blue]: [yellow]{cmd_string}[/yellow]",
|
222
|
+
decorate=decorate_logs,
|
223
|
+
)
|
188
224
|
|
189
225
|
if record_sub_job_time is not None:
|
190
226
|
# Capture how long this run took
|