runem 0.0.28__py3-none-any.whl → 0.0.30__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.0.28
1
+ 0.0.30
runem/cli.py CHANGED
@@ -1,4 +1,5 @@
1
1
  """CLI interface for runem project."""
2
+
2
3
  import sys
3
4
 
4
5
  from runem.runem import timed_main
runem/command_line.py CHANGED
@@ -6,7 +6,7 @@ import typing
6
6
 
7
7
  from runem.config_metadata import ConfigMetadata
8
8
  from runem.informative_dict import InformativeDict
9
- from runem.log import log
9
+ from runem.log import error, log
10
10
  from runem.runem_version import get_runem_version
11
11
  from runem.types import JobNames, OptionConfig, OptionsWritable
12
12
  from runem.utils import printable_set
@@ -22,7 +22,12 @@ def parse_args(
22
22
 
23
23
  Returns the parsed args, the jobs_names_to_run, job_phases_to_run, job_tags_to_run
24
24
  """
25
- parser = argparse.ArgumentParser(description="Runs the Lursight Lang test-suite")
25
+ parser = argparse.ArgumentParser(
26
+ add_help=False, description="Runs the Lursight Lang test-suite"
27
+ )
28
+ parser.add_argument(
29
+ "-H", "--help", action="help", help="show this help message and exit"
30
+ )
26
31
 
27
32
  job_group = parser.add_argument_group("jobs")
28
33
  all_job_names: JobNames = set(name for name in config_metadata.all_job_names)
@@ -112,6 +117,26 @@ def parse_args(
112
117
  required=False,
113
118
  )
114
119
 
120
+ parser.add_argument(
121
+ "-f",
122
+ "--modified-files-only",
123
+ dest="check_modified_files_only",
124
+ help="only use files that have changed",
125
+ action=argparse.BooleanOptionalAction,
126
+ default=False,
127
+ required=False,
128
+ )
129
+
130
+ parser.add_argument(
131
+ "-h",
132
+ "--git-head-files-only",
133
+ dest="check_head_files_only",
134
+ help="fast run of files",
135
+ action=argparse.BooleanOptionalAction,
136
+ default=False,
137
+ required=False,
138
+ )
139
+
115
140
  parser.add_argument(
116
141
  "--procs",
117
142
  "-j",
@@ -206,9 +231,9 @@ def _validate_filters(
206
231
  for name, name_list in (("--jobs", args.jobs), ("--not-jobs", args.jobs_excluded)):
207
232
  for job_name in name_list:
208
233
  if job_name not in config_metadata.all_job_names:
209
- log(
234
+ error(
210
235
  (
211
- f"ERROR: invalid job-name '{job_name}' for {name}, "
236
+ f"invalid job-name '{job_name}' for {name}, "
212
237
  f"choose from one of {printable_set(config_metadata.all_job_names)}"
213
238
  )
214
239
  )
@@ -218,9 +243,9 @@ def _validate_filters(
218
243
  for name, tag_list in (("--tags", args.tags), ("--not-tags", args.tags_excluded)):
219
244
  for tag in tag_list:
220
245
  if tag not in config_metadata.all_job_tags:
221
- log(
246
+ error(
222
247
  (
223
- f"ERROR: invalid tag '{tag}' for {name}, "
248
+ f"invalid tag '{tag}' for {name}, "
224
249
  f"choose from one of {printable_set(config_metadata.all_job_tags)}"
225
250
  )
226
251
  )
@@ -233,9 +258,9 @@ def _validate_filters(
233
258
  ):
234
259
  for phase in phase_list:
235
260
  if phase not in config_metadata.all_job_phases:
236
- log(
261
+ error(
237
262
  (
238
- f"ERROR: invalid phase '{phase}' for {name}, "
263
+ f"invalid phase '{phase}' for {name}, "
239
264
  f"choose from one of {printable_set(config_metadata.all_job_phases)}"
240
265
  )
241
266
  )
runem/config.py CHANGED
@@ -5,9 +5,9 @@ import typing
5
5
  import yaml
6
6
  from packaging.version import Version
7
7
 
8
- from runem.log import log
8
+ from runem.log import error, log
9
9
  from runem.runem_version import get_runem_version
10
- from runem.types import Config, GlobalConfig, GlobalSerialisedConfig
10
+ from runem.types import Config, GlobalConfig, GlobalSerialisedConfig, UserConfigMetadata
11
11
 
12
12
  CFG_FILE_YAML = pathlib.Path(".runem.yml")
13
13
 
@@ -40,20 +40,53 @@ def _search_up_multiple_dirs_for_file(
40
40
  return None
41
41
 
42
42
 
43
- def _find_cfg() -> pathlib.Path:
44
- """Searches up from the cwd for a .runem.yml config file."""
43
+ def _find_config_file(
44
+ config_filename: typing.Union[str, pathlib.Path]
45
+ ) -> typing.Tuple[typing.Optional[pathlib.Path], typing.Tuple[pathlib.Path, ...]]:
46
+ """Searches up from the cwd for the given config file-name."""
45
47
  start_dirs = (pathlib.Path(".").absolute(),)
46
48
  cfg_candidate: typing.Optional[pathlib.Path] = _search_up_multiple_dirs_for_file(
47
- start_dirs, CFG_FILE_YAML
49
+ start_dirs, config_filename
48
50
  )
51
+ return cfg_candidate, start_dirs
52
+
53
+
54
+ def _find_project_cfg() -> pathlib.Path:
55
+ """Searches up from the cwd for the project .runem.yml config file."""
56
+ cfg_candidate: typing.Optional[pathlib.Path]
57
+ start_dirs: typing.Tuple[pathlib.Path, ...]
58
+ cfg_candidate, start_dirs = _find_config_file(config_filename=CFG_FILE_YAML)
59
+
49
60
  if cfg_candidate:
50
61
  return cfg_candidate
51
62
 
52
63
  # error out and exit as we currently require the cfg file as it lists jobs.
53
- log(f"ERROR: Config not found! Looked from {start_dirs}")
64
+ error(f"Config not found! Looked from {start_dirs}")
54
65
  sys.exit(1)
55
66
 
56
67
 
68
+ def _find_local_configs() -> typing.List[pathlib.Path]:
69
+ """Searches for all user-configs and returns the found ones.
70
+
71
+ TODO: add some priorities to the files, such that
72
+ - .runem.local.yml has lowest priority
73
+ - $HOME/.runem.user.yml is applied after .local
74
+ - .runem.user.yml overloads all others
75
+ """
76
+ local_configs: typing.List[pathlib.Path] = []
77
+ for config_filename in (".runem.local.yml", ".runem.user.yml"):
78
+ cfg_candidate: typing.Optional[pathlib.Path]
79
+ cfg_candidate, _ = _find_config_file(config_filename)
80
+ if cfg_candidate:
81
+ local_configs.append(cfg_candidate)
82
+
83
+ user_home_config: pathlib.Path = pathlib.Path("~/.runem.user.yml")
84
+ if user_home_config.exists():
85
+ local_configs.append(user_home_config)
86
+
87
+ return local_configs
88
+
89
+
57
90
  def _conform_global_config_types(
58
91
  all_config: Config,
59
92
  ) -> typing.Tuple[Config, typing.Optional[GlobalConfig]]:
@@ -77,9 +110,8 @@ def _conform_global_config_types(
77
110
  return all_config, global_config
78
111
 
79
112
 
80
- def load_config() -> typing.Tuple[Config, pathlib.Path]:
81
- """Finds and loads the .runem.yml file."""
82
- cfg_filepath: pathlib.Path = _find_cfg()
113
+ def load_and_parse_config(cfg_filepath: pathlib.Path) -> Config:
114
+ """For the given config file pass, project or user, load it & parse/conform it."""
83
115
  with cfg_filepath.open("r+", encoding="utf-8") as config_file_handle:
84
116
  all_config = yaml.full_load(config_file_handle)
85
117
 
@@ -104,5 +136,22 @@ def load_config() -> typing.Tuple[Config, pathlib.Path]:
104
136
  )
105
137
  )
106
138
  sys.exit(1)
139
+ return conformed_config
140
+
141
+
142
+ def load_project_config() -> typing.Tuple[Config, pathlib.Path]:
143
+ """Finds and loads the .runem.yml file for the current project."""
144
+ cfg_filepath: pathlib.Path = _find_project_cfg()
145
+ conformed_config: Config = load_and_parse_config(cfg_filepath)
107
146
 
108
147
  return conformed_config, cfg_filepath
148
+
149
+
150
+ def load_user_configs() -> UserConfigMetadata:
151
+ """Returns the user-local configs, that extend/override runem behaviour."""
152
+ user_configs: typing.List[typing.Tuple[Config, pathlib.Path]] = []
153
+ user_config_paths: typing.List[pathlib.Path] = _find_local_configs()
154
+ for config_path in user_config_paths:
155
+ user_config: Config = load_and_parse_config(config_path)
156
+ user_configs.append((user_config, config_path))
157
+ return user_configs
runem/config_metadata.py CHANGED
@@ -1,5 +1,6 @@
1
1
  import argparse
2
2
  import pathlib
3
+ import typing
3
4
 
4
5
  from runem.informative_dict import InformativeDict
5
6
  from runem.types import (
@@ -13,6 +14,9 @@ from runem.types import (
13
14
  TagFileFilters,
14
15
  )
15
16
 
17
+ if typing.TYPE_CHECKING: # pragma: no cover
18
+ from runem.hook_manager import HookManager
19
+
16
20
 
17
21
  class ConfigMetadata:
18
22
  """Full metadata about what can and should be run."""
@@ -20,6 +24,7 @@ class ConfigMetadata:
20
24
  phases: OrderedPhases # the phases and orders to run them in
21
25
  options_config: OptionConfigs # the options to add to the cli and pass to jobs
22
26
  file_filters: TagFileFilters # which files to get for which tag
27
+ hook_manager: "HookManager" # the hooks register for the run
23
28
  jobs: PhaseGroupedJobs # the jobs to be run ordered by phase
24
29
  all_job_names: JobNames # the set of job-names
25
30
  all_job_phases: JobPhases # the set of job-phases (should be subset of 'phases')
@@ -39,6 +44,7 @@ class ConfigMetadata:
39
44
  phases: OrderedPhases,
40
45
  options_config: OptionConfigs,
41
46
  file_filters: TagFileFilters,
47
+ hook_manager: "HookManager",
42
48
  jobs: PhaseGroupedJobs,
43
49
  all_job_names: JobNames,
44
50
  all_job_phases: JobPhases,
@@ -48,6 +54,8 @@ class ConfigMetadata:
48
54
  self.phases = phases
49
55
  self.options_config = options_config
50
56
  self.file_filters = file_filters
57
+ # initialise all the registered call-back hooks
58
+ self.hook_manager = hook_manager
51
59
  self.jobs = jobs
52
60
  self.all_job_names = all_job_names
53
61
  self.all_job_phases = all_job_phases
runem/config_parse.py CHANGED
@@ -6,14 +6,20 @@ from collections import defaultdict
6
6
  from collections.abc import Iterable
7
7
 
8
8
  from runem.config_metadata import ConfigMetadata
9
+ from runem.hook_manager import HookManager
9
10
  from runem.job import Job
10
11
  from runem.job_wrapper import get_job_wrapper
11
- from runem.log import log
12
+ from runem.log import error, log, warn
12
13
  from runem.types import (
13
14
  Config,
14
15
  ConfigNodes,
16
+ FunctionNotFound,
15
17
  GlobalConfig,
16
18
  GlobalSerialisedConfig,
19
+ HookConfig,
20
+ HookName,
21
+ Hooks,
22
+ HookSerialisedConfig,
17
23
  JobConfig,
18
24
  JobNames,
19
25
  JobPhases,
@@ -61,7 +67,31 @@ def _parse_global_config(
61
67
  return phases, options, file_filters
62
68
 
63
69
 
64
- def _parse_job(
70
+ def parse_hook_config(
71
+ hook: HookConfig,
72
+ cfg_filepath: pathlib.Path,
73
+ ) -> None:
74
+ """Get the hook information, verifying validity."""
75
+ try:
76
+ if not HookManager.is_valid_hook_name(hook["hook_name"]):
77
+ raise ValueError(
78
+ f"invalid hook-name '{str(hook['hook_name'])}'. "
79
+ f"Valid hook names are: {[hook.value for hook in HookName]}"
80
+ )
81
+ # cast the hook-name to a HookName type
82
+ hook["hook_name"] = HookName(hook["hook_name"])
83
+ get_job_wrapper(hook, cfg_filepath)
84
+ except KeyError as err:
85
+ raise ValueError(
86
+ f"hook config entry is missing '{err.args[0]}' key. Have {tuple(hook.keys())}"
87
+ ) from err
88
+ except FunctionNotFound as err:
89
+ raise FunctionNotFound(
90
+ f"Whilst loading job '{str(hook['hook_name'])}'. {str(err)}"
91
+ ) from err
92
+
93
+
94
+ def _parse_job( # noqa: C901
65
95
  cfg_filepath: pathlib.Path,
66
96
  job: JobConfig,
67
97
  in_out_tags: JobTags,
@@ -69,22 +99,43 @@ def _parse_job(
69
99
  in_out_job_names: JobNames,
70
100
  in_out_phases: JobPhases,
71
101
  phase_order: OrderedPhases,
102
+ warn_missing_phase: bool = True,
72
103
  ) -> None:
73
104
  """Parse an individual job."""
74
105
  job_name: str = Job.get_job_name(job)
75
106
  job_names_used = job_name in in_out_job_names
76
107
  if job_names_used:
77
- log("ERROR: duplicate job label!")
78
- log(f"\t'{job['label']}' is used twice or more in {str(cfg_filepath)}")
108
+ error(
109
+ "duplicate job label!"
110
+ f"\t'{job['label']}' is used twice or more in {str(cfg_filepath)}"
111
+ )
79
112
  sys.exit(1)
80
113
 
81
- # try and load the function _before_ we schedule it's execution
82
- get_job_wrapper(job, cfg_filepath)
114
+ try:
115
+ # try and load the function _before_ we schedule it's execution
116
+ get_job_wrapper(job, cfg_filepath)
117
+ except FunctionNotFound as err:
118
+ raise FunctionNotFound(
119
+ f"Whilst loading job '{job['label']}'. {str(err)}"
120
+ ) from err
121
+
83
122
  try:
84
123
  phase_id: PhaseName = job["when"]["phase"]
85
124
  except KeyError:
86
- fallback_phase = phase_order[0]
87
- log(f"WARNING: no phase found for '{job_name}', using '{fallback_phase}'")
125
+ try:
126
+ fallback_phase = phase_order[0]
127
+ if warn_missing_phase:
128
+ warn(f"no phase found for '{job_name}', using '{fallback_phase}'")
129
+ except IndexError:
130
+ fallback_phase = "<NO PHASES FOUND>"
131
+ if warn_missing_phase:
132
+ warn(
133
+ (
134
+ f"no phases found for '{job_name}', "
135
+ f"or in '{str(cfg_filepath)}', "
136
+ f"using '{fallback_phase}'"
137
+ )
138
+ )
88
139
  phase_id = fallback_phase
89
140
  in_out_jobs_by_phase[phase_id].append(job)
90
141
 
@@ -169,7 +220,18 @@ def parse_job_config(
169
220
  ) from err
170
221
 
171
222
 
172
- def parse_config(config: Config, cfg_filepath: pathlib.Path) -> ConfigMetadata:
223
+ def parse_config(
224
+ config: Config, cfg_filepath: pathlib.Path, hooks_only: bool = False
225
+ ) -> typing.Tuple[
226
+ Hooks, # hooks:
227
+ OrderedPhases, # phase_order:
228
+ OptionConfigs, # options:
229
+ TagFileFilters, # file_filters:
230
+ PhaseGroupedJobs, # jobs_by_phase:
231
+ JobNames, # job_names:
232
+ JobPhases, # job_phases:
233
+ JobTags, # tags:
234
+ ]:
173
235
  """Validates and restructure the config to make it more convenient to use."""
174
236
  jobs_by_phase: PhaseGroupedJobs = defaultdict(list)
175
237
  job_names: JobNames = set()
@@ -180,6 +242,7 @@ def parse_config(config: Config, cfg_filepath: pathlib.Path) -> ConfigMetadata:
180
242
  phase_order: OrderedPhases = ()
181
243
  options: OptionConfigs = ()
182
244
  file_filters: TagFileFilters = {}
245
+ hooks: Hooks = defaultdict(list)
183
246
 
184
247
  # first search for the global config
185
248
  for entry in config:
@@ -204,12 +267,28 @@ def parse_config(config: Config, cfg_filepath: pathlib.Path) -> ConfigMetadata:
204
267
  phase_order, options, file_filters = _parse_global_config(global_config)
205
268
  continue
206
269
 
270
+ # we apply a type-ignore here as we know (for now) that jobs have "job"
271
+ # keys and global configs have "global" keys
272
+ isinstance_hooks: bool = "hook" in entry
273
+ if isinstance_hooks:
274
+ hook_entry: HookSerialisedConfig = entry # type: ignore # see above
275
+ hook: HookConfig = hook_entry["hook"]
276
+ parse_hook_config(hook, cfg_filepath)
277
+
278
+ # if we get here we have validated the hook, add it to the hooks list
279
+ hook_name: HookName = hook["hook_name"]
280
+ hooks[hook_name].append(hook)
281
+
282
+ # continue to the next element and do NOT error
283
+ continue
284
+
207
285
  # not a global or a job entry, what is it
208
- raise RuntimeError(f"invalid 'job' or 'global' config entry, {entry}")
286
+ raise RuntimeError(f"invalid 'job', 'hook, or 'global' config entry, {entry}")
209
287
 
210
288
  if not phase_order:
211
- log("WARNING: phase ordering not configured! Runs will be non-deterministic!")
212
- phase_order = tuple(job_phases)
289
+ if not hooks_only:
290
+ warn("phase ordering not configured! Runs will be non-deterministic!")
291
+ phase_order = tuple(job_phases)
213
292
 
214
293
  # now parse out the job_configs
215
294
  for entry in config:
@@ -228,13 +307,109 @@ def parse_config(config: Config, cfg_filepath: pathlib.Path) -> ConfigMetadata:
228
307
  in_out_phases=job_phases,
229
308
  phase_order=phase_order,
230
309
  )
310
+ return (
311
+ hooks,
312
+ phase_order,
313
+ options,
314
+ file_filters,
315
+ jobs_by_phase,
316
+ job_names,
317
+ job_phases,
318
+ tags,
319
+ )
320
+
231
321
 
232
- # tags = tags.union(("python", "es", "firebase_funcs"))
322
+ def generate_config(
323
+ cfg_filepath: pathlib.Path,
324
+ hooks: Hooks,
325
+ phase_order: OrderedPhases,
326
+ verbose: bool,
327
+ options: OptionConfigs,
328
+ file_filters: TagFileFilters,
329
+ jobs_by_phase: PhaseGroupedJobs,
330
+ job_names: JobNames,
331
+ job_phases: JobPhases,
332
+ tags: JobTags,
333
+ ) -> ConfigMetadata:
334
+ """Constructs the ConfigMetadata from parsed config parts."""
233
335
  return ConfigMetadata(
234
336
  cfg_filepath,
235
337
  phase_order,
236
338
  options,
237
339
  file_filters,
340
+ HookManager(hooks, verbose),
341
+ jobs_by_phase,
342
+ job_names,
343
+ job_phases,
344
+ tags,
345
+ )
346
+
347
+
348
+ def _load_user_hooks_from_config(
349
+ user_config: Config, cfg_filepath: pathlib.Path
350
+ ) -> Hooks:
351
+ hooks: Hooks
352
+ (
353
+ hooks,
354
+ _,
355
+ _,
356
+ _,
357
+ _,
358
+ _,
359
+ _,
360
+ _,
361
+ ) = parse_config(user_config, cfg_filepath, hooks_only=True)
362
+ return hooks
363
+
364
+
365
+ def load_config_metadata(
366
+ config: Config,
367
+ cfg_filepath: pathlib.Path,
368
+ user_configs: typing.List[typing.Tuple[Config, pathlib.Path]],
369
+ verbose: bool = False,
370
+ ) -> ConfigMetadata:
371
+ hooks: Hooks
372
+ phase_order: OrderedPhases
373
+ options: OptionConfigs
374
+ file_filters: TagFileFilters
375
+ jobs_by_phase: PhaseGroupedJobs
376
+ job_names: JobNames
377
+ job_phases: JobPhases
378
+ tags: JobTags
379
+ (
380
+ hooks,
381
+ phase_order,
382
+ options,
383
+ file_filters,
384
+ jobs_by_phase,
385
+ job_names,
386
+ job_phases,
387
+ tags,
388
+ ) = parse_config(config, cfg_filepath)
389
+
390
+ user_config: Config
391
+ user_config_path: pathlib.Path
392
+ for user_config, user_config_path in user_configs:
393
+ user_hooks: Hooks = _load_user_hooks_from_config(user_config, user_config_path)
394
+ if user_hooks:
395
+ if verbose:
396
+ log(f"hooks: loading user hooks from '{str(user_config_path)}'")
397
+ hook_name: HookName
398
+ hooks_for_name: typing.List[HookConfig]
399
+ for hook_name, hooks_for_name in user_hooks.items():
400
+ hooks[hook_name].extend(hooks_for_name)
401
+ if verbose:
402
+ log(
403
+ f"hooks:\tadded {len(hooks_for_name)} user hooks for '{str(hook_name)}'"
404
+ )
405
+
406
+ return generate_config(
407
+ cfg_filepath,
408
+ hooks,
409
+ phase_order,
410
+ verbose,
411
+ options,
412
+ file_filters,
238
413
  jobs_by_phase,
239
414
  job_names,
240
415
  job_phases,
runem/files.py CHANGED
@@ -1,6 +1,7 @@
1
1
  import re
2
2
  import typing
3
3
  from collections import defaultdict
4
+ from pathlib import Path
4
5
  from subprocess import check_output as subprocess_check_output
5
6
 
6
7
  from runem.config_metadata import ConfigMetadata
@@ -23,14 +24,38 @@ def find_files(config_metadata: ConfigMetadata) -> FilePathListLookup:
23
24
  """
24
25
  file_lists: FilePathListLookup = defaultdict(list)
25
26
 
26
- file_paths: typing.List[str] = (
27
- subprocess_check_output(
28
- "git ls-files",
29
- shell=True,
27
+ file_paths: typing.List[str] = []
28
+
29
+ if config_metadata.args.check_modified_files_only:
30
+ file_paths = (
31
+ subprocess_check_output(
32
+ "git diff --name-only",
33
+ shell=True,
34
+ )
35
+ .decode("utf-8")
36
+ .splitlines()
37
+ )
38
+ elif config_metadata.args.check_head_files_only:
39
+ # Fetching modified and added files from the HEAD commit that are still on disk
40
+ file_paths = (
41
+ subprocess_check_output(
42
+ "git diff-tree --no-commit-id --name-only -r HEAD",
43
+ shell=True,
44
+ )
45
+ .decode("utf-8")
46
+ .splitlines()
47
+ )
48
+ file_paths = [file_path for file_path in file_paths if Path(file_path).exists()]
49
+ else:
50
+ # fall-back to all files
51
+ file_paths = (
52
+ subprocess_check_output(
53
+ "git ls-files",
54
+ shell=True,
55
+ )
56
+ .decode("utf-8")
57
+ .splitlines()
30
58
  )
31
- .decode("utf-8")
32
- .splitlines()
33
- )
34
59
  _bucket_file_by_tag(
35
60
  file_paths,
36
61
  config_metadata,
runem/hook_manager.py ADDED
@@ -0,0 +1,116 @@
1
+ import typing
2
+ from collections import defaultdict
3
+
4
+ from runem.config_metadata import ConfigMetadata
5
+ from runem.job import Job
6
+ from runem.job_execute import job_execute
7
+ from runem.log import log
8
+ from runem.types import (
9
+ FilePathListLookup,
10
+ HookConfig,
11
+ HookName,
12
+ Hooks,
13
+ HooksStore,
14
+ JobConfig,
15
+ )
16
+
17
+
18
+ class HookManager:
19
+ hooks_store: HooksStore
20
+
21
+ def __init__(self, hooks: Hooks, verbose: bool) -> None:
22
+ self.hooks_store: HooksStore = defaultdict(list)
23
+ self.initialise_hooks(hooks, verbose)
24
+
25
+ @staticmethod
26
+ def is_valid_hook_name(hook_name: typing.Union[HookName, str]) -> bool:
27
+ """Returns True/False depending on hook-name validity."""
28
+ if isinstance(hook_name, str):
29
+ try:
30
+ HookName(hook_name) # lookup by value
31
+ return True
32
+ except ValueError:
33
+ return False
34
+ # the type is a HookName
35
+ if not isinstance(hook_name, HookName):
36
+ return False
37
+ return True
38
+
39
+ def register_hook(
40
+ self, hook_name: HookName, hook_config: HookConfig, verbose: bool
41
+ ) -> None:
42
+ """Registers a hook_config to a specific hook-type."""
43
+ if not self.is_valid_hook_name(hook_name):
44
+ raise ValueError(f"Hook {hook_name} does not exist.")
45
+ self.hooks_store[hook_name].append(hook_config)
46
+ if verbose:
47
+ log(
48
+ f"hooks: registered hook for '{hook_name}', "
49
+ f"have {len(self.hooks_store[hook_name])}: "
50
+ f"{Job.get_job_name(hook_config)}" # type: ignore[arg-type]
51
+ )
52
+
53
+ def deregister_hook(
54
+ self, hook_name: HookName, hook_config: HookConfig, verbose: bool
55
+ ) -> None:
56
+ """Deregisters a hook_config from a specific hook-type."""
57
+ if not (
58
+ hook_name in self.hooks_store and hook_config in self.hooks_store[hook_name]
59
+ ):
60
+ raise ValueError(f"Function not found in hook {hook_name}.")
61
+ self.hooks_store[hook_name].remove(hook_config)
62
+ if verbose:
63
+ log(
64
+ f"hooks: deregistered hooks for '{hook_name}', "
65
+ f"have {len(self.hooks_store[hook_name])}"
66
+ )
67
+
68
+ def invoke_hooks(
69
+ self,
70
+ hook_name: HookName,
71
+ config_metadata: ConfigMetadata,
72
+ **kwargs: typing.Any,
73
+ ) -> None:
74
+ """Invokes all functions registered to a specific hook."""
75
+ hooks: typing.List[HookConfig] = self.hooks_store.get(hook_name, [])
76
+ if config_metadata.args.verbose:
77
+ log(f"hooks: invoking {len(hooks)} hooks for '{hook_name}'")
78
+
79
+ hook_config: HookConfig
80
+ for hook_config in hooks:
81
+ job_config: JobConfig = {
82
+ "label": str(hook_name),
83
+ "ctx": None,
84
+ "when": {"phase": str(hook_name), "tags": {str(hook_name)}},
85
+ }
86
+ if "addr" in hook_config:
87
+ job_config["addr"] = hook_config["addr"]
88
+ if "command" in hook_config:
89
+ job_config["command"] = hook_config["command"]
90
+ file_lists: FilePathListLookup = defaultdict(list)
91
+ file_lists[str(hook_name)] = [__file__]
92
+ job_execute(
93
+ job_config,
94
+ running_jobs={},
95
+ config_metadata=config_metadata,
96
+ file_lists=file_lists,
97
+ **kwargs,
98
+ )
99
+
100
+ if config_metadata.args.verbose:
101
+ log(f"hooks: done invoking '{hook_name}'")
102
+
103
+ def initialise_hooks(self, hooks: Hooks, verbose: bool) -> None:
104
+ """Initialised the hook with the configured data."""
105
+ if verbose:
106
+ num_hooks: int = sum(len(hooks_list) for hooks_list in hooks.values())
107
+ if num_hooks:
108
+ log(f"hooks: initialising {num_hooks} hooks")
109
+ for hook_name in hooks:
110
+ hook: HookConfig
111
+ if verbose:
112
+ log(
113
+ f"hooks:\tinitialising {len(hooks[hook_name])} hooks for '{hook_name}'"
114
+ )
115
+ for hook in hooks[hook_name]:
116
+ self.register_hook(hook_name, hook, verbose)