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 CHANGED
@@ -1 +1 @@
1
- 0.4.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=False, # Do NOT print markup e.g. `[blink]Don't Panic![/blink]`.
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=False, # Do NOT print markup e.g. `[blink]Don't Panic![/blink]`.
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
- "Only jobs with the given tags. "
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, cfg_filepath: pathlib.Path, hooks_only: bool = False
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
- warn("phase ordering not configured! Runs will be non-deterministic!")
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, cfg_filepath: pathlib.Path
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(user_config, user_config_path)
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
- log(f"WARNING: skipping job '{label}', no files for job")
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(msg: str = "", decorate: bool = True, end: typing.Optional[str] = None) -> None:
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
- msg = f"runem: {msg}"
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(f"report: {str(job_report_url_info[0])}: {str(job_report_url_info[1])}")
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 RunCommandBadExitCode(RuntimeError):
16
- pass
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 RunCommandUnhandledError(RuntimeError):
20
- pass
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
- # Split stdout into lines, noting if it ends with a newline
35
- ends_with_newline = stdout.endswith("\n")
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(f"running: start: {label}: {cmd_string}")
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(f"\tallowed return ids are: {valid_exit_strs}")
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(f"ENV OVERRIDES: {env_overrides_as_string} {cmd_string}")
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(parse_stdout(line, prefix=f"{label}: "))
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=f"{label}: ERROR: ") if process else ""
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: test: FATAL: command failed: {label}"
175
- f"\n\t{env_overrides_as_string}{cmd_string}"
176
- f"\nERROR"
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"\nERROR END"
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(f"running: done: {label}: {cmd_string}")
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