runem 0.6.0__py3-none-any.whl → 0.7.1__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.6.0
1
+ 0.7.1
runem/blocking_print.py CHANGED
@@ -21,6 +21,9 @@ def _reset_console() -> Console:
21
21
  # `highlight` is what colourises string and number in print() calls.
22
22
  # We do not want this to be auto-magic.
23
23
  highlight=False,
24
+ # `soft_wrap=True` disables word-wrap & cropping by default:
25
+ # - `soft_wrap` reads like a misnomer to me
26
+ soft_wrap=True,
24
27
  )
25
28
  return RICH_CONSOLE
26
29
 
@@ -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
  )
runem/command_line.py CHANGED
@@ -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
  )
runem/config.py CHANGED
@@ -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]
runem/config_parse.py CHANGED
@@ -11,7 +11,7 @@ from runem.job import Job
11
11
  from runem.job_wrapper import get_job_wrapper
12
12
  from runem.log import error, log, warn
13
13
  from runem.types.common import JobNames, JobPhases, JobTags, OrderedPhases, PhaseName
14
- from runem.types.errors import FunctionNotFound
14
+ from runem.types.errors import FunctionNotFound, SystemExitBad
15
15
  from runem.types.filters import TagFileFilter, TagFileFilters
16
16
  from runem.types.hooks import HookName
17
17
  from runem.types.runem_config import (
@@ -81,9 +81,8 @@ def parse_hook_config(
81
81
  f"hook config entry is missing '{err.args[0]}' key. Have {tuple(hook.keys())}"
82
82
  ) from err
83
83
  except FunctionNotFound as err:
84
- raise FunctionNotFound(
85
- f"Whilst loading job '{str(hook['hook_name'])}'. {str(err)}"
86
- ) from err
84
+ error(f"Whilst loading hook '{str(hook['hook_name'])}'. {str(err)}")
85
+ raise SystemExitBad(2) from err
87
86
 
88
87
 
89
88
  def _parse_job( # noqa: C901
@@ -110,9 +109,8 @@ def _parse_job( # noqa: C901
110
109
  # try and load the function _before_ we schedule it's execution
111
110
  get_job_wrapper(job, cfg_filepath)
112
111
  except FunctionNotFound as err:
113
- raise FunctionNotFound(
114
- f"Whilst loading job '{job['label']}'. {str(err)}"
115
- ) from err
112
+ error(f"Whilst loading job '{job['label']}'. {str(err)}")
113
+ raise SystemExitBad(2) from err
116
114
 
117
115
  try:
118
116
  phase_id: PhaseName = job["when"]["phase"]
@@ -162,7 +160,8 @@ def parse_job_config(
162
160
  ("cwd" in job["ctx"]) and (job["ctx"]["cwd"] is not None)
163
161
  )
164
162
  if (not have_ctw_cwd) or isinstance(
165
- job["ctx"]["cwd"], str # type: ignore # handled above
163
+ job["ctx"]["cwd"], # type: ignore # handled above
164
+ str,
166
165
  ):
167
166
  # if
168
167
  # - 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.")
runem/informative_dict.py CHANGED
@@ -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")
runem/job.py CHANGED
@@ -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
runem/job_execute.py CHANGED
@@ -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
runem/job_filter.py CHANGED
@@ -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
@@ -56,9 +55,9 @@ def _load_python_function_from_module(
56
55
  except AttributeError as err:
57
56
  raise FunctionNotFound(
58
57
  (
59
- f"ERROR! Check that function '{function_to_load}' "
60
- f"exists in '{str(module_file_path)}' as expected in "
61
- f"your config at '{str(cfg_filepath)}"
58
+ f"Check that function '[blue]{function_to_load}[/blue]' "
59
+ f"exists in '[blue]{str(module_file_path)}[/blue]' as expected in "
60
+ f"your config at '[blue]{str(cfg_filepath)}[/blue]'"
62
61
  )
63
62
  ) from err
64
63
  return function
@@ -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
 
runem/log.py CHANGED
@@ -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
 
runem/report.py CHANGED
@@ -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 = (
runem/run_command.py CHANGED
@@ -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:
runem/runem.py CHANGED
@@ -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__":
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/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/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
@@ -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.1
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)
@@ -1,41 +1,46 @@
1
- runem/VERSION,sha256=l6XW5UCmEg0Jw53bZn4Ojiusf8wv_vgTuC4I_WA2W84,6
1
+ runem/VERSION,sha256=kCMRx7s4-hZY_0A972FY7nN4_p1uLihPocOHNcMb0ws,6
2
2
  runem/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
3
3
  runem/__main__.py,sha256=dsOiVZegpfK9JOs5n7UmbX5iwwbj7iFkEbLoVeEgAn4,136
4
4
  runem/base.py,sha256=EZfR7FIlwEdU9Vfe47Wk2DOO8GQqpKxxLNKp6YHueZ4,316
5
- runem/blocking_print.py,sha256=2nCvc10zXl1DRJldkruKthKjfmZKErcmxQzt3pjmN-c,2289
5
+ runem/blocking_print.py,sha256=UKU_BM7wzPvn6RKw_tFPw4Lzpjdtbwq5MIaAdL1_zN8,2435
6
6
  runem/cli.py,sha256=wEt_Jnumhl8SiOdKdSJzLkJpWv6n3_Odhi_HeIixr1k,134
7
- runem/command_line.py,sha256=qkZFCCq9hUl6RO398SJzoigv8di5jGw2sdNwgTVBdd8,14474
8
- runem/config.py,sha256=UiEU0Jyg5qjrNStvasWYjMOABQHhpZjbPiX3-sH_CMg,5969
7
+ runem/command_line.py,sha256=Q5xH7kGzc3YpIbGpU43B_pwKvp-LchSW7IgfGnf2Ke0,14437
8
+ runem/config.py,sha256=PWj4wj90WrRiGuXOUy6mpevJXQM57epDWG5NYDrGK2w,6049
9
9
  runem/config_metadata.py,sha256=krDomUcADsAeUQrxwNmOS58eeaNIlqmhWIKWv8mUH4A,3300
10
- runem/config_parse.py,sha256=zXQ4rpj-igQufB5JtTsI1mOE_gBTdBcI2hI6HWU28gg,13830
10
+ runem/config_parse.py,sha256=A1pLfEL5CEaexH-Kn_PYthAa9mwmOS5UrD0kEbDcqPo,13843
11
+ runem/config_validate.py,sha256=gtatObD1qBkEK-0CJ2rJPT5A7EBTWE_9V_AiGZZ1FQI,1424
11
12
  runem/files.py,sha256=59boeFvUANYOS-PllIjeKIht6lNINZ43WxahDg90oAc,4392
12
13
  runem/hook_manager.py,sha256=H0TL3HCqU2mgKm_-dgCD7TsK5T1bLT4g7x6kpytMPhU,4350
13
- runem/informative_dict.py,sha256=U7p9z78UwOT4TAfng1iDXCEyeYz6C-XZlx9Z1pWNVrI,1548
14
- runem/job.py,sha256=NOdRQnGePPyYdmIR_6JKVFzp9nbgNGetpE13bHEHaf4,3442
15
- runem/job_execute.py,sha256=-76IJI0PDU_XdQiDxTKUfOHEno9pixxQb_zi58rFumo,4702
16
- runem/job_filter.py,sha256=7vgG4YWJ9gyGBFjV7QbSojG5ofYoszAmxXx9HnMLkHo,5384
14
+ runem/informative_dict.py,sha256=4UUE_RU6zEX1JFFlVUK5EMdmPdJ5ZOo5scU93fYi0iU,1831
15
+ runem/job.py,sha256=SX_uHaFxocFm2QT2hRTfAv7mADnMj2th85L_JzYwS-4,3440
16
+ runem/job_execute.py,sha256=Sn5v7KoyCOw2FH-bJaur_zYChwmvCXhmdZ692B3SZA8,4792
17
+ runem/job_filter.py,sha256=4KMjsI5-tiK0b90RTlivxm5Xdt0gWdRz30voqqL_ijI,5374
17
18
  runem/job_runner_simple_command.py,sha256=iP5an6yixW8o4C0ZBtu6csb-oVK3Q62ZZgtHBmxlXaU,2428
18
19
  runem/job_wrapper.py,sha256=q5GtopZ5vhSJ581rwU4-lF9KnbL3ZYgOC8fqaCnXD_g,983
19
- runem/job_wrapper_python.py,sha256=rx7J_N-JXs8GgMq7Sla7B9s_ZAfofKUhEnzgMcq_bts,4303
20
- runem/log.py,sha256=6r6HIJyvp19P6PZNo93qdIxE0SpTAsY4ELrETBl1dC4,1363
20
+ runem/job_wrapper_python.py,sha256=9rM6OXs0rNRVNwZ7OVB6L0K7RocEpiG0N-qIMuprjRA,4335
21
+ runem/log.py,sha256=MEGWEBeFs0YVkr_U9UXnl4Isqc5MvGbEn_I3fo-V35Q,1422
21
22
  runem/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
22
- runem/report.py,sha256=beye95AV9Lop_K7eOoLY40vKt0VGkIdg-ig-SmJ_5MY,9083
23
- runem/run_command.py,sha256=R77jqAtrXPBkFtT7QXJfnQkivU4h01M8G0Q2sjWY6Gs,7691
24
- runem/runem.py,sha256=6XZqf59_ZMyrBaK1FlaLzMXYNoDQsTUcbFqTbpZL1ik,13192
23
+ runem/report.py,sha256=qiGu0PPcbeFh9KJHtFKjH_LQHDfYuDgLVbUA81fNsLI,9124
24
+ runem/run_command.py,sha256=jz-vP9yw3WDM_0mc5KrBpqFcASSQUFB9zz9u_le-m8A,7692
25
+ runem/runem.py,sha256=Lm0mg8oTe7pCvQVZob6KR6zq7G5xoMTjrCfhBlCQwZQ,13436
25
26
  runem/runem_version.py,sha256=MbETwZO2Tb1Y3hX_OYZjKepEMKA1cjNvr-7Cqhz6e3s,271
27
+ runem/schema.yml,sha256=LmQVtVbziaH_Z3aaE5-njR7qPYTP8qqruNQt8rVfv5M,3720
26
28
  runem/utils.py,sha256=MEYfox09rLvb6xmay_3rV1cWmdqMbhaAjOomYGNk15k,602
27
- runem/cli/initialise_options.py,sha256=zx_EduWQk7yGBr3XUNrffHCSInPv05edFItHLnlo9dk,918
29
+ runem/yaml_utils.py,sha256=RyAvEp679JvavE0Kbs263Ofh8_tTXXHUdtblSeHovfU,554
30
+ runem/yaml_validation.py,sha256=j8vnufeV8FRwg15wbitL_QLP4nh25Rx1L_14Oc2puwU,877
31
+ runem/cli/initialise_options.py,sha256=91QjAHdfhUNI9nWVDnHAqNshxCXytXQfbsEaQo3FMLc,917
28
32
  runem/types/__init__.py,sha256=0bWG7hE7VeqJ2oIu-xhrqQud8hcNp6WNbF3uMfT_n9g,314
29
33
  runem/types/common.py,sha256=gPMSoJ3yRUYjHnoviRrpSg0gRwsGLFGWGpbTWkq4jX0,279
30
- runem/types/errors.py,sha256=rbM5BA6UhY1X7Q0OZLUNsG7JXAjgNFTG5KQuqPNuZm8,103
34
+ runem/types/errors.py,sha256=9A4V5qT3ofolqRxrqBI9i9jX3r60x8nQnQnt9w1Y3co,403
31
35
  runem/types/filters.py,sha256=8R5fyMssN0ISGBilJhEtbdHFl6OP7uI51WKkB5SH6EA,255
32
- runem/types/hooks.py,sha256=lgrv5QAuHCEzr5dXDj4-azNcs63addY9zdrGWj5zv_s,292
36
+ runem/types/hooks.py,sha256=9Q5THuBEH0Asdx5cj0caNmO54RckwR0tT3AgRXFROhs,292
33
37
  runem/types/options.py,sha256=y8_hyWYvhalC9-kZbvoDtxm0trZgyyGcswQqfuQy_pM,265
34
38
  runem/types/runem_config.py,sha256=qG_bghm5Nr-ZTbaZbf1v8Fx447V-hgEvvRy5NZ3t-Io,5141
35
- runem/types/types_jobs.py,sha256=wqiiBmRIJDbGlKcfOqewHGKx350w0p4_7pysMm7xGmo,4906
39
+ runem/types/types_jobs.py,sha256=99b2TLwBiqjOurxRWWU7E9BlphR0KketUUSqUD_Kh5c,4432
40
+ runem-0.7.1.dist-info/licenses/LICENSE,sha256=awOCsWJ58m_2kBQwBUGWejVqZm6wuRtCL2hi9rfa0X4,1211
36
41
  scripts/test_hooks/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
37
42
  scripts/test_hooks/json_validators.py,sha256=N2FyWcpjWzfFGycXLo-ecNLJkxTFPPbqPfVBcJLBlb4,967
38
- scripts/test_hooks/py.py,sha256=YUbwNny7NPmv2bY7k7YcbJ-jRcnNfjQajE9Hn1MLaBc,8821
43
+ scripts/test_hooks/py.py,sha256=Nku44p6ZqJb5d9uo2bPfpeqf8g1LDHRDqL4ou0Y0G_k,10520
39
44
  scripts/test_hooks/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
40
45
  scripts/test_hooks/runem_hooks.py,sha256=FJMuDBEOz3dr9gBW3WW6yKbUJs_LFXb3klpqSzCAZRk,628
41
46
  scripts/test_hooks/yarn.py,sha256=1QsG1rKAclpZoqp86ntkuvzYaYN4UkEvO0JhO2Kf5C8,1082
@@ -44,9 +49,8 @@ tests/data/help_output.3.10.txt,sha256=5TUpNITVL6pD5BpFAl-Orh3vkOpStveijZzvgJuI_
44
49
  tests/data/help_output.3.11.txt,sha256=ycrF-xKgdQ8qrWzkkR-vbHe7NulUTsCsS0_Gda8xYDs,4162
45
50
  tests/test_types/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
46
51
  tests/test_types/test_public_api.py,sha256=QHiwt7CetQur65JSbFRnOzQxhCJkX5MVLymHHVd_6yc,160
47
- runem-0.6.0.dist-info/LICENSE,sha256=awOCsWJ58m_2kBQwBUGWejVqZm6wuRtCL2hi9rfa0X4,1211
48
- runem-0.6.0.dist-info/METADATA,sha256=00_gsiXfiHPCwcZumnmOU2KjaUTvR6kF9c9nr4p2YHc,5892
49
- runem-0.6.0.dist-info/WHEEL,sha256=In9FTNxeP60KnTkGw7wk6mJPYd_dQSjEZmXdBdMCI-8,91
50
- runem-0.6.0.dist-info/entry_points.txt,sha256=nu0g_vBeuPihYtimbtlNusxWovylMppvJ8UxdJlJfvM,46
51
- runem-0.6.0.dist-info/top_level.txt,sha256=NkdxkwLKNNhxItveR2KqNqTshTZ268m5D7SjJEmG4-Y,20
52
- runem-0.6.0.dist-info/RECORD,,
52
+ runem-0.7.1.dist-info/METADATA,sha256=wX6dRVH8E905TMINRtxhwtGoVeyaokkjrQcg-2h1TgY,5894
53
+ runem-0.7.1.dist-info/WHEEL,sha256=pxyMxgL8-pra_rKaQ4drOZAegBVuX-G_4nRHjjgWbmo,91
54
+ runem-0.7.1.dist-info/entry_points.txt,sha256=nu0g_vBeuPihYtimbtlNusxWovylMppvJ8UxdJlJfvM,46
55
+ runem-0.7.1.dist-info/top_level.txt,sha256=NkdxkwLKNNhxItveR2KqNqTshTZ268m5D7SjJEmG4-Y,20
56
+ runem-0.7.1.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (75.8.0)
2
+ Generator: setuptools (79.0.0)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
scripts/test_hooks/py.py CHANGED
@@ -9,7 +9,69 @@ from runem.run_command import RunCommandUnhandledError, run_command
9
9
  from runem.types import FilePathList, JobKwargs, JobName, JobReturnData, Options
10
10
 
11
11
 
12
- def _job_py_code_reformat(
12
+ def _job_py_code_ruff_reformat(
13
+ **kwargs: typing.Any,
14
+ ) -> None:
15
+ """Runs python formatting code in serial order as one influences the other."""
16
+ label: JobName = kwargs["label"]
17
+ options: Options = kwargs["options"]
18
+ python_files: FilePathList = kwargs["file_list"]
19
+
20
+ # put into 'check' mode if requested on the command line
21
+ extra_args = []
22
+ if options["check-only"]:
23
+ extra_args.append("--check")
24
+
25
+ if not options["ruff"]:
26
+ # Do not run `ruff` if opted-out
27
+ return
28
+
29
+ # If ruff is enabled we do NOT run black etc. because ruff does that
30
+ # for us, faster and better.
31
+ ruff_format_cmd = [
32
+ "python3",
33
+ "-m",
34
+ "ruff",
35
+ "format",
36
+ *extra_args,
37
+ *python_files,
38
+ ]
39
+ kwargs["label"] = f"{label} ruff"
40
+ run_command(cmd=ruff_format_cmd, **kwargs)
41
+
42
+
43
+ def _job_py_ruff_lint(
44
+ **kwargs: typing.Any,
45
+ ) -> None:
46
+ """Runs python formatting code in serial order as one influences the other."""
47
+ label: JobName = kwargs["label"]
48
+ options: Options = kwargs["options"]
49
+ python_files: FilePathList = kwargs["file_list"]
50
+
51
+ # try to auto-fix issues (one benefit of ruff over flake8 etc.)
52
+ extra_args = []
53
+ if options["fix"]:
54
+ extra_args.append("--fix")
55
+
56
+ if not options["ruff"]:
57
+ # Do not run `ruff` if opted-out
58
+ return
59
+
60
+ # If ruff is enabled we do NOT run black etc. because ruff does that
61
+ # for us, faster and better.
62
+ ruff_lint_cmd = [
63
+ "python3",
64
+ "-m",
65
+ "ruff",
66
+ "check",
67
+ *extra_args,
68
+ *python_files,
69
+ ]
70
+ kwargs["label"] = f"{label} ruff"
71
+ run_command(cmd=ruff_lint_cmd, **kwargs)
72
+
73
+
74
+ def _job_py_code_reformat_deprecated(
13
75
  **kwargs: Unpack[JobKwargs],
14
76
  ) -> None:
15
77
  """Runs python formatting code in serial order as one influences the other."""