runem 0.5.0__py3-none-any.whl → 0.7.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/runem.py CHANGED
@@ -19,6 +19,8 @@ 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
+
23
+ import contextlib
22
24
  import multiprocessing
23
25
  import os
24
26
  import pathlib
@@ -30,10 +32,9 @@ from datetime import timedelta
30
32
  from itertools import repeat
31
33
  from multiprocessing.managers import DictProxy, ValueProxy
32
34
  from timeit import default_timer as timer
33
- from types import TracebackType
34
35
 
35
- from rich.console import Console, ConsoleOptions, ConsoleRenderable, RenderResult
36
36
  from rich.spinner import Spinner
37
+ from rich.status import Status
37
38
  from rich.text import Text
38
39
 
39
40
  from runem.blocking_print import RICH_CONSOLE
@@ -46,7 +47,9 @@ from runem.job_execute import job_execute
46
47
  from runem.job_filter import filter_jobs
47
48
  from runem.log import error, log, warn
48
49
  from runem.report import report_on_run
50
+ from runem.run_command import RunemJobError
49
51
  from runem.types.common import OrderedPhases, PhaseName
52
+ from runem.types.errors import SystemExitBad
50
53
  from runem.types.filters import FilePathListLookup
51
54
  from runem.types.hooks import HookName
52
55
  from runem.types.runem_config import Config, Jobs, PhaseGroupedJobs
@@ -56,7 +59,7 @@ from runem.types.types_jobs import (
56
59
  JobRunMetadatasByPhase,
57
60
  JobTiming,
58
61
  )
59
- from runem.utils import printable_set
62
+ from runem.utils import printable_set_coloured
60
63
 
61
64
 
62
65
  def _determine_run_parameters(argv: typing.List[str]) -> ConfigMetadata:
@@ -67,7 +70,6 @@ def _determine_run_parameters(argv: typing.List[str]) -> ConfigMetadata:
67
70
 
68
71
  Return a ConfigMetadata object with all the required information.
69
72
  """
70
-
71
73
  # Because we want to be able to show logging whilst parsing .runem.yml config, we
72
74
  # need to check the state of the logging-verbosity switches here, manually, as well.
73
75
  verbose = "--verbose" in argv
@@ -92,38 +94,8 @@ def _determine_run_parameters(argv: typing.List[str]) -> ConfigMetadata:
92
94
  return config_metadata
93
95
 
94
96
 
95
- class DummySpinner(ConsoleRenderable): # pragma: no cover
96
- """A dummy spinner for when spinners are disabled."""
97
-
98
- def __init__(self) -> None:
99
- self.text = ""
100
-
101
- def __rich__(self) -> Text:
102
- """Return a rich Text object for rendering."""
103
- return Text(self.text)
104
-
105
- def __rich_console__(
106
- self, console: Console, options: ConsoleOptions
107
- ) -> RenderResult:
108
- """Yield an empty string or placeholder text."""
109
- yield Text(self.text)
110
-
111
- def __enter__(self) -> None:
112
- """Support for context manager."""
113
- pass
114
-
115
- def __exit__(
116
- self,
117
- exc_type: typing.Optional[typing.Type[BaseException]],
118
- exc_value: typing.Optional[BaseException],
119
- traceback: typing.Optional[TracebackType],
120
- ) -> None:
121
- """Support for context manager."""
122
- pass
123
-
124
-
125
97
  def _update_progress(
126
- label: str,
98
+ phase: str,
127
99
  running_jobs: typing.Dict[str, str],
128
100
  completed_jobs: typing.Dict[str, str],
129
101
  all_jobs: Jobs,
@@ -134,36 +106,44 @@ def _update_progress(
134
106
  """Updates progress report periodically for running tasks.
135
107
 
136
108
  Args:
137
- label (str): The identifier.
109
+ phase (str): The currently running phase.
138
110
  running_jobs (Dict[str, str]): The currently running jobs.
111
+ completed_jobs (Dict[str, str]): The jobs that have finished work.
139
112
  all_jobs (Jobs): All jobs, encompassing both completed and running jobs.
140
113
  is_running (ValueProxy[bool]): Flag indicating if jobs are still running.
141
114
  num_workers (int): Indicates the number of workers performing the jobs.
115
+ show_spinner (bool): Whether to show the animated spinner or not.
142
116
  """
143
- # Using the `rich` module to show a loading spinner on console
144
- spinner: typing.Union[Spinner, DummySpinner]
145
- if show_spinner:
146
- spinner = Spinner("dots", text="Starting tasks...")
147
- else:
148
- spinner = DummySpinner()
149
-
150
117
  last_running_jobs_set: typing.Set[str] = set()
151
118
 
152
- with RICH_CONSOLE.status(spinner):
119
+ # Using the `rich` module to show a loading spinner on console
120
+ spinner_ctx: typing.Union[Status, typing.ContextManager[None]] = (
121
+ RICH_CONSOLE.status(Spinner("dots", text="Starting tasks..."))
122
+ if show_spinner
123
+ else contextlib.nullcontext()
124
+ )
125
+
126
+ with spinner_ctx:
153
127
  while is_running.value:
154
128
  running_jobs_set: typing.Set[str] = set(running_jobs.values())
155
129
 
156
130
  # Progress report
157
131
  progress: str = f"{len(completed_jobs)}/{len(all_jobs)}"
158
- running_jobs_list = printable_set(
159
- running_jobs_set
132
+ running_jobs_list = printable_set_coloured(
133
+ running_jobs_set,
134
+ "blue",
160
135
  ) # Reflect current running jobs accurately
161
- report: str = f"{label}: {progress}({num_workers}): {running_jobs_list}"
136
+ report: str = (
137
+ f"[green]{phase}[/green]: {progress}({num_workers}): "
138
+ f"{running_jobs_list}"
139
+ )
162
140
  if show_spinner:
163
- spinner.text = report
141
+ assert isinstance(spinner_ctx, Status)
142
+ spinner_ctx.update(Text.from_markup(report))
164
143
  else:
165
144
  if last_running_jobs_set != running_jobs_set:
166
145
  RICH_CONSOLE.log(report)
146
+ last_running_jobs_set = running_jobs_set
167
147
 
168
148
  # Sleep for reduced CPU usage
169
149
  time.sleep(0.1)
@@ -176,7 +156,7 @@ def _process_jobs(
176
156
  phase: PhaseName,
177
157
  jobs: Jobs,
178
158
  show_spinner: bool,
179
- ) -> typing.Optional[BaseException]:
159
+ ) -> typing.Optional[RunemJobError]:
180
160
  """Execute each given job asynchronously.
181
161
 
182
162
  This is where the major real-world time savings happen, and it could be
@@ -196,12 +176,12 @@ def _process_jobs(
196
176
  num_concurrent_procs: int = min(max_num_concurrent_procs, len(jobs))
197
177
  log(
198
178
  (
199
- f"Running '{phase}' with {num_concurrent_procs} workers (of "
179
+ f"Running '[green]{phase}[/green]' with {num_concurrent_procs} workers (of "
200
180
  f"{max_num_concurrent_procs} max) processing {len(jobs)} jobs"
201
181
  )
202
182
  )
203
183
 
204
- subprocess_error: typing.Optional[BaseException] = None
184
+ subprocess_error: typing.Optional[RunemJobError] = None
205
185
 
206
186
  with multiprocessing.Manager() as manager:
207
187
  running_jobs: DictProxy[typing.Any, typing.Any] = manager.dict()
@@ -235,7 +215,7 @@ def _process_jobs(
235
215
  repeat(file_lists),
236
216
  ),
237
217
  )
238
- except BaseException as err: # pylint: disable=broad-exception-caught
218
+ except RunemJobError as err: # pylint: disable=broad-exception-caught
239
219
  subprocess_error = err
240
220
  finally:
241
221
  # Signal the terminal_writer process to exit
@@ -251,7 +231,7 @@ def _process_jobs_by_phase(
251
231
  filtered_jobs_by_phase: PhaseGroupedJobs,
252
232
  in_out_job_run_metadatas: JobRunMetadatasByPhase,
253
233
  show_spinner: bool,
254
- ) -> typing.Optional[BaseException]:
234
+ ) -> typing.Optional[RunemJobError]:
255
235
  """Execute each job asynchronously, grouped by phase.
256
236
 
257
237
  Whilst it is conceptually useful to group jobs by 'phase', Phases are
@@ -275,7 +255,7 @@ def _process_jobs_by_phase(
275
255
  if config_metadata.args.verbose:
276
256
  log(f"Running Phase {phase}")
277
257
 
278
- failure_exception: typing.Optional[BaseException] = _process_jobs(
258
+ failure_exception: typing.Optional[RunemJobError] = _process_jobs(
279
259
  config_metadata,
280
260
  file_lists,
281
261
  in_out_job_run_metadatas,
@@ -293,7 +273,7 @@ def _process_jobs_by_phase(
293
273
 
294
274
 
295
275
  MainReturnType = typing.Tuple[
296
- ConfigMetadata, JobRunMetadatasByPhase, typing.Optional[BaseException]
276
+ ConfigMetadata, JobRunMetadatasByPhase, typing.Optional[RunemJobError]
297
277
  ]
298
278
 
299
279
 
@@ -316,8 +296,8 @@ def _main(
316
296
  log(f"found {len(file_lists)} batches, ", end="")
317
297
  for tag in sorted(file_lists.keys()):
318
298
  file_list = file_lists[tag]
319
- log(f"{len(file_list)} '{tag}' files, ", decorate=False, end="")
320
- log(decorate=False) # new line
299
+ log(f"{len(file_list)} '{tag}' files, ", prefix=False, end="")
300
+ log(prefix=False) # new line
321
301
 
322
302
  filtered_jobs_by_phase: PhaseGroupedJobs = filter_jobs(
323
303
  config_metadata=config_metadata,
@@ -333,7 +313,7 @@ def _main(
333
313
 
334
314
  start = timer()
335
315
 
336
- failure_exception: typing.Optional[BaseException] = _process_jobs_by_phase(
316
+ failure_exception: typing.Optional[RunemJobError] = _process_jobs_by_phase(
337
317
  config_metadata,
338
318
  file_lists,
339
319
  filtered_jobs_by_phase,
@@ -362,7 +342,7 @@ def timed_main(argv: typing.List[str]) -> None:
362
342
  start = timer()
363
343
  config_metadata: ConfigMetadata
364
344
  job_run_metadatas: JobRunMetadatasByPhase
365
- failure_exception: typing.Optional[BaseException]
345
+ failure_exception: typing.Optional[RunemJobError]
366
346
  config_metadata, job_run_metadatas, failure_exception = _main(argv)
367
347
  phase_run_oder: OrderedPhases = config_metadata.phases
368
348
  end = timer()
@@ -372,14 +352,15 @@ def timed_main(argv: typing.List[str]) -> None:
372
352
  system_time_spent, wall_clock_time_saved = report_on_run(
373
353
  phase_run_oder, job_run_metadatas, time_taken
374
354
  )
375
- message: str = "DONE: runem took"
355
+ message: str = "[green bold]DONE[/green bold]: runem took"
376
356
  if failure_exception:
377
- message = "FAILED: your jobs failed after"
357
+ message = "[red bold]FAILED[/red bold]: your jobs failed after"
378
358
  log(
379
359
  (
380
360
  f"{message}: {time_taken.total_seconds()}s, "
381
- f"saving you {wall_clock_time_saved.total_seconds()}s, "
382
- f"without runem you would have waited {system_time_spent.total_seconds()}s"
361
+ f"saving you [green]{wall_clock_time_saved.total_seconds()}s[/green], "
362
+ "without runem you would have waited "
363
+ f"[red]{system_time_spent.total_seconds()}s[/red]"
383
364
  )
384
365
  )
385
366
 
@@ -392,7 +373,8 @@ def timed_main(argv: typing.List[str]) -> None:
392
373
  if failure_exception is not None:
393
374
  # we got a failure somewhere, now that we've reported the timings we
394
375
  # re-raise.
395
- raise failure_exception
376
+ error(failure_exception.stdout)
377
+ raise SystemExitBad(1) from failure_exception
396
378
 
397
379
 
398
380
  if __name__ == "__main__":
runem/schema.yml ADDED
@@ -0,0 +1,137 @@
1
+ #%RAML 1.0 (← just a comment so VS Code picks up YAML)
2
+ $schema: "https://json-schema.org/draft/2020-12/schema"
3
+ $title: Runem pipeline definition
4
+ $defs:
5
+ # ----- common pieces -------------------------------------------------------
6
+ phase:
7
+ type: string
8
+
9
+ addr:
10
+ type: object
11
+ required: [file, function]
12
+ additionalProperties: false
13
+ properties:
14
+ file: { type: string, minLength: 1 }
15
+ function: { type: string, minLength: 1 }
16
+
17
+ ctx:
18
+ type: object
19
+ additionalProperties: false
20
+ properties:
21
+ cwd:
22
+ oneOf:
23
+ - type: string
24
+ - type: array
25
+ minItems: 1
26
+ items: { type: string, minLength: 1 }
27
+ params:
28
+ type: object # free‑form kv‑pairs for hooks
29
+ additionalProperties: true
30
+
31
+ when:
32
+ type: object
33
+ required: [phase]
34
+ additionalProperties: false
35
+ properties:
36
+ phase: { $ref: "#/$defs/phase" }
37
+ tags:
38
+ type: array
39
+ items: { type: string, minLength: 1 }
40
+ uniqueItems: true
41
+
42
+ # ----- top‑level entity types ---------------------------------------------
43
+ config:
44
+ type: object
45
+ required: []
46
+ additionalProperties: false
47
+ properties:
48
+ min_version:
49
+ type: string
50
+
51
+ phases:
52
+ type: array
53
+ minItems: 1
54
+ items: { $ref: "#/$defs/phase" }
55
+ uniqueItems: true
56
+
57
+ files:
58
+ type: [array, 'null']
59
+ minItems: 0
60
+ items:
61
+ type: object
62
+ required: [filter]
63
+ additionalProperties: false
64
+ properties:
65
+ filter:
66
+ type: object
67
+ required: [tag, regex]
68
+ additionalProperties: false
69
+ properties:
70
+ tag: { type: string, minLength: 1 }
71
+ regex: { type: string, minLength: 1 } # leave pattern‑checking to the engine
72
+
73
+ options:
74
+ type: [array, 'null']
75
+ minItems: 0
76
+ items:
77
+ type: object
78
+ required: [option]
79
+ additionalProperties: false
80
+ properties:
81
+ option:
82
+ type: object
83
+ required: [name, type, default, desc]
84
+ additionalProperties: false
85
+ properties:
86
+ name: { type: string, minLength: 1 }
87
+ alias: { type: string, minLength: 1 }
88
+ desc: { type: string, minLength: 1 }
89
+ type:
90
+ const: bool # always "bool" per sample
91
+ default: { type: boolean }
92
+
93
+ hook:
94
+ type: object
95
+ required: [hook_name]
96
+ oneOf:
97
+ - required: [command]
98
+ - required: [addr]
99
+ additionalProperties: false
100
+ properties:
101
+ hook_name: { type: string, minLength: 1 }
102
+ addr: { $ref: "#/$defs/addr" }
103
+ command: { type: string, minLength: 1 }
104
+
105
+ job:
106
+ type: object
107
+ oneOf:
108
+ - required: [command]
109
+ - required: [addr]
110
+ additionalProperties: false
111
+ properties:
112
+ label: { type: string, minLength: 1 }
113
+ addr: { $ref: "#/$defs/addr" }
114
+ command: { type: string, minLength: 1 }
115
+ ctx: { $ref: "#/$defs/ctx" }
116
+ when: { $ref: "#/$defs/when" }
117
+ oneOf:
118
+ - required: [addr] # either addr
119
+ - required: [command] # or command, but not both
120
+ not:
121
+ anyOf:
122
+ - required: [addr, command] # forbid both together
123
+
124
+ # ---------- ROOT -------------------------------------------------------------
125
+ type: array
126
+ minItems: 1
127
+ items:
128
+ type: object
129
+ additionalProperties: false
130
+ oneOf:
131
+ - required: [config]
132
+ - required: [hook]
133
+ - required: [job]
134
+ properties:
135
+ config: { $ref: "#/$defs/config" }
136
+ hook: { $ref: "#/$defs/hook" }
137
+ job: { $ref: "#/$defs/job" }
runem/types/__init__.py CHANGED
@@ -1,11 +1,12 @@
1
1
  from runem.types.common import FilePathList, JobName
2
2
  from runem.types.options import Options
3
- from runem.types.types_jobs import HookKwargs, JobKwargs, JobReturnData
3
+ from runem.types.types_jobs import HookKwargs, JobKwargs, JobReturn, JobReturnData
4
4
 
5
5
  __all__ = [
6
6
  "FilePathList",
7
7
  "HookKwargs",
8
8
  "JobName",
9
+ "JobReturn",
9
10
  "JobReturnData",
10
11
  "Options",
11
12
  "JobKwargs",
runem/types/errors.py CHANGED
@@ -1,4 +1,14 @@
1
+ from typing import Optional
2
+
3
+
1
4
  class FunctionNotFound(ValueError):
2
5
  """Thrown when the test-function cannot be found."""
3
6
 
4
7
  pass
8
+
9
+
10
+ class SystemExitBad(SystemExit):
11
+ def __init__(self, code: Optional[int] = None) -> None:
12
+ super().__init__()
13
+ self.code = 1 if code is None else code # non-zero bad exit code
14
+ assert self.code > 0, "A bad exit code should be non-zero and >0"
runem/types/hooks.py CHANGED
@@ -4,7 +4,7 @@ import enum
4
4
  class HookName(enum.Enum):
5
5
  """List supported hooks.
6
6
 
7
- TODO:
7
+ Todo:
8
8
  - before all tasks are run, after config is read
9
9
  - BEFORE_ALL = "before-all"
10
10
  - after all tasks are done, before reporting
runem/types/types_jobs.py CHANGED
@@ -1,28 +1,25 @@
1
- """
1
+ """Job‑typing helpers.
2
+
3
+ Cross‑version advice
4
+ --------------------
5
+ * Type variadic keyword arguments as **kwargs: Unpack[KwArgsT] for clarity.
6
+ * Always import Unpack from ``typing_extensions``.
7
+ - Std‑lib Unpack appears only in Py 3.12+.
8
+ - ``typing_extensions`` works on 3.9‑3.12, so one import path keeps
9
+ mypy/pyright happy without conditional logic.
10
+
11
+ Example:
12
+ ~~~~~~~
13
+ from typing_extensions import TypedDict, Unpack
14
+
15
+
16
+ class SaveKwArgs(TypedDict):
17
+ path: str
18
+ overwrite: bool
19
+
2
20
 
3
- Some note on Unpack and kwargs:
4
- We *try* to strongly type `**kwargs` for clarity.
5
- We have tried several ways to define a Generic type that encapsulates
6
- `**kwargs: SingleType`
7
- ... but none of the solutions worked with python 3.9 -> 3.12 and mypy 1.9.0,
8
- so we have to recommend instead using:
9
- `**kwargs: Unpack[KwArgsType]`
10
-
11
- For this to work across versions of python where support for Unpack changes;
12
- for example `Unpack` is a python 3.12 feature, but available in the
13
- `typing_extensions` module.
14
-
15
- So, for now, it looks like we get away with importing `Unpack` from the
16
- `typing_extensions` module, even in python 3.12, so we will use, and
17
- recommend using, the `typing_extensions` of `Unpack`, until it becomes
18
- obsolete.
19
-
20
- Alternatively, we can use the following, but it's unnecessarily verbose.
21
-
22
- if sys.version_info >= (3, 12): # pragma: no coverage
23
- from typing import Unpack
24
- else: # pragma: no coverage
25
- from typing_extensions import Unpack
21
+ def save_job(**kwargs: Unpack[SaveKwArgs]) -> None:
22
+ ...
26
23
  """
27
24
 
28
25
  import pathlib
runem/utils.py CHANGED
@@ -4,3 +4,15 @@ import typing
4
4
  def printable_set(some_set: typing.Set[typing.Any]) -> str:
5
5
  """Get a printable, deterministic string version of a set."""
6
6
  return ", ".join([f"'{set_item}'" for set_item in sorted(list(some_set))])
7
+
8
+
9
+ def printable_set_coloured(some_set: typing.Set[typing.Any], colour: str) -> str:
10
+ """`printable_set` but elements are surrounded with colour mark-up.
11
+
12
+ Parameters:
13
+ some_set: a set of anything
14
+ colour: a `rich` Console supported colour
15
+ """
16
+ return ", ".join(
17
+ [f"'[{colour}]{set_item}[/{colour}]'" for set_item in sorted(list(some_set))]
18
+ )
runem/yaml_utils.py ADDED
@@ -0,0 +1,19 @@
1
+ import pathlib
2
+ import typing
3
+
4
+ import yaml
5
+
6
+
7
+ def load_yaml_object(yaml_file: pathlib.Path) -> typing.Any:
8
+ """Loads using full_load, a yaml file.
9
+
10
+ This is likely to have safety concerns in non-trusted projects.
11
+
12
+ Returns:
13
+ YAML Loader object: the full PyYAML loader object.
14
+ """
15
+ # Do a full, untrusted load of the runem config
16
+ # TODO: work out safety concerns of this
17
+ with yaml_file.open("r+", encoding="utf-8") as file_handle:
18
+ full_yaml_object: typing.Any = yaml.full_load(file_handle)
19
+ return full_yaml_object
@@ -0,0 +1,28 @@
1
+ from typing import Any, List
2
+
3
+ from jsonschema import Draft202012Validator, ValidationError
4
+
5
+ # For now just return the raw ValidationErrors as a list
6
+ ValidationErrors = List[ValidationError]
7
+
8
+
9
+ def validate_yaml(yaml_data: Any, schema: Any) -> ValidationErrors:
10
+ """Validates the give yaml data against the given schema, returning any errors.
11
+
12
+ We use more future-looking validation so that we can have richer and more
13
+ descriptive schema.
14
+
15
+ Params:
16
+ instance: JSON data loaded via `load_json` or similar
17
+ schema: schema object compatible with a Draft202012Validator
18
+
19
+ Returns:
20
+ ValidationErrors: a sorted list of errors in the file, empty if none found
21
+ """
22
+ validator = Draft202012Validator(schema)
23
+ errors: ValidationErrors = sorted(
24
+ validator.iter_errors(yaml_data),
25
+ key=lambda e: e.path,
26
+ )
27
+
28
+ return errors