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 +1 -1
- runem/cli.py +1 -0
- runem/command_line.py +33 -8
- runem/config.py +58 -9
- runem/config_metadata.py +8 -0
- runem/config_parse.py +188 -13
- runem/files.py +32 -7
- runem/hook_manager.py +116 -0
- runem/job_execute.py +49 -26
- runem/job_filter.py +2 -2
- runem/job_runner_simple_command.py +7 -1
- runem/job_wrapper.py +11 -5
- runem/job_wrapper_python.py +7 -7
- runem/log.py +8 -0
- runem/report.py +145 -34
- runem/run_command.py +18 -0
- runem/runem.py +46 -19
- runem/types.py +62 -5
- {runem-0.0.28.dist-info → runem-0.0.30.dist-info}/METADATA +25 -34
- runem-0.0.30.dist-info/RECORD +33 -0
- {runem-0.0.28.dist-info → runem-0.0.30.dist-info}/WHEEL +1 -1
- runem-0.0.28.dist-info/RECORD +0 -32
- {runem-0.0.28.dist-info → runem-0.0.30.dist-info}/LICENSE +0 -0
- {runem-0.0.28.dist-info → runem-0.0.30.dist-info}/entry_points.txt +0 -0
- {runem-0.0.28.dist-info → runem-0.0.30.dist-info}/top_level.txt +0 -0
runem/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
0.0.
|
1
|
+
0.0.30
|
runem/cli.py
CHANGED
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(
|
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
|
-
|
234
|
+
error(
|
210
235
|
(
|
211
|
-
f"
|
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
|
-
|
246
|
+
error(
|
222
247
|
(
|
223
|
-
f"
|
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
|
-
|
261
|
+
error(
|
237
262
|
(
|
238
|
-
f"
|
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
|
44
|
-
|
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,
|
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
|
-
|
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
|
81
|
-
"""
|
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
|
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
|
-
|
78
|
-
|
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
|
-
|
82
|
-
|
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
|
-
|
87
|
-
|
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(
|
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
|
-
|
212
|
-
|
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
|
-
|
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
|
-
|
28
|
-
|
29
|
-
|
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)
|