MATE-runtime 1.0.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.
Files changed (93) hide show
  1. MATE/__init__.py +22 -0
  2. MATE/__main__.py +7 -0
  3. MATE/cli/__init__.py +181 -0
  4. MATE/cli/commands.py +573 -0
  5. MATE/cli/workdir.py +571 -0
  6. MATE/compat/__init__.py +1 -0
  7. MATE/compat/interface.py +248 -0
  8. MATE/compat/sizes.py +144 -0
  9. MATE/config/__init__.py +1 -0
  10. MATE/config/axes.py +310 -0
  11. MATE/config/builders.py +498 -0
  12. MATE/config/calendar.py +371 -0
  13. MATE/config/canonical.py +543 -0
  14. MATE/config/criteria.py +396 -0
  15. MATE/config/events.py +183 -0
  16. MATE/config/invariance.py +227 -0
  17. MATE/config/loader.py +498 -0
  18. MATE/config/model.py +883 -0
  19. MATE/config/resolve.py +943 -0
  20. MATE/config/runtable.py +342 -0
  21. MATE/config/seeds.py +344 -0
  22. MATE/config/stages.py +632 -0
  23. MATE/config/structure.py +635 -0
  24. MATE/config/toml.py +237 -0
  25. MATE/conformance/__init__.py +14 -0
  26. MATE/conformance/baseline.py +250 -0
  27. MATE/conformance/checks.py +1604 -0
  28. MATE/conformance/sampler.py +179 -0
  29. MATE/errors.py +236 -0
  30. MATE/evaluation/__init__.py +1 -0
  31. MATE/evaluation/budget.py +160 -0
  32. MATE/evaluation/invalids.py +181 -0
  33. MATE/evaluation/requests.py +148 -0
  34. MATE/execution/__init__.py +1 -0
  35. MATE/execution/accelerator.py +60 -0
  36. MATE/execution/base.py +643 -0
  37. MATE/execution/choose.py +152 -0
  38. MATE/execution/devices.py +108 -0
  39. MATE/execution/process.py +377 -0
  40. MATE/execution/serial.py +37 -0
  41. MATE/execution/thread.py +106 -0
  42. MATE/execution/vectorised.py +32 -0
  43. MATE/launch.py +267 -0
  44. MATE/loop/__init__.py +6 -0
  45. MATE/loop/continuous.py +172 -0
  46. MATE/loop/driver.py +783 -0
  47. MATE/loop/events.py +266 -0
  48. MATE/loop/generational.py +140 -0
  49. MATE/loop/owns_loop.py +333 -0
  50. MATE/loop/subpops.py +508 -0
  51. MATE/modules/__init__.py +1 -0
  52. MATE/modules/construct.py +198 -0
  53. MATE/modules/discovery.py +238 -0
  54. MATE/modules/registry.py +970 -0
  55. MATE/modules/software.py +593 -0
  56. MATE/modules/versions.py +72 -0
  57. MATE/numerics.py +159 -0
  58. MATE/observation/__init__.py +10 -0
  59. MATE/observation/control.py +199 -0
  60. MATE/observation/hub.py +451 -0
  61. MATE/observation/records.py +125 -0
  62. MATE/observation/sinks.py +313 -0
  63. MATE/provenance.py +396 -0
  64. MATE/readout/__init__.py +10 -0
  65. MATE/readout/catalogue.py +282 -0
  66. MATE/readout/front.py +151 -0
  67. MATE/readout/readout.py +927 -0
  68. MATE/readout/series.py +155 -0
  69. MATE/readout/statistics.py +202 -0
  70. MATE/run/__init__.py +1 -0
  71. MATE/run/checkpointing.py +266 -0
  72. MATE/run/contexts.py +553 -0
  73. MATE/run/declarations.py +240 -0
  74. MATE/run/plan.py +464 -0
  75. MATE/run/preconditions.py +165 -0
  76. MATE/run/run.py +991 -0
  77. MATE/run/specs.py +319 -0
  78. MATE/run/times.py +103 -0
  79. MATE/scheduling/__init__.py +214 -0
  80. MATE/scheduling/local.py +119 -0
  81. MATE/scheduling/slurm.py +380 -0
  82. MATE/storage/__init__.py +1 -0
  83. MATE/storage/checkpoint.py +414 -0
  84. MATE/storage/codec.py +315 -0
  85. MATE/storage/database.py +359 -0
  86. MATE/storage/lease.py +397 -0
  87. MATE/storage/rundir.py +248 -0
  88. mate_runtime-1.0.0.dist-info/METADATA +58 -0
  89. mate_runtime-1.0.0.dist-info/RECORD +93 -0
  90. mate_runtime-1.0.0.dist-info/WHEEL +4 -0
  91. mate_runtime-1.0.0.dist-info/entry_points.txt +2 -0
  92. mate_runtime-1.0.0.dist-info/licenses/COPYING +674 -0
  93. mate_runtime-1.0.0.dist-info/licenses/COPYING.LESSER +165 -0
MATE/cli/commands.py ADDED
@@ -0,0 +1,573 @@
1
+ """The commands of the ``MATE`` command line ([X] §24, [C] §21, design §3.14).
2
+
3
+ Each ``cmd_<name>`` function takes the parsed arguments and returns the exit status: 0
4
+ success, 1 refusal (software, interface, preconditions, deferred feature), 2 run failures,
5
+ 3 usage error. Reports go to stdout, errors to stderr.
6
+ """
7
+
8
+ import argparse
9
+ import sys
10
+ import tempfile
11
+ from collections.abc import Sequence
12
+ from pathlib import Path
13
+
14
+ from packaging.utils import canonicalize_name
15
+
16
+ from MATE.compat.interface import Outcome, check_interface
17
+ from MATE.config.loader import LoadedProtocol, load_protocol
18
+ from MATE.config.model import Value
19
+ from MATE.config.toml import parse_toml_value
20
+ from MATE.errors import ProtocolRefused, Refusal, SoftwareCompatibilityError
21
+ from MATE.launch import exec_run, launch
22
+ from MATE.modules import registry
23
+ from MATE.modules.construct import (ModuleConstructionError, construct_module,
24
+ declared_defaults, missing_parameters)
25
+ from MATE.modules.discovery import ModuleEntry, installed_modules
26
+ from MATE.modules.software import ModuleRef, check_software, render
27
+ from MATE.observation.control import send_command
28
+ from MATE.run.plan import LaunchResult
29
+ from MATE.run.specs import NormalisedSpecs, is_collective, normalise_specs
30
+ from MATE.scheduling.slurm import SubmissionError
31
+ from MATE.storage.lease import RunBusyError
32
+
33
+ from MATE.cli import workdir
34
+
35
+ __all__ = [
36
+ "REPLAY_REFUSAL",
37
+ "DASHBOARD_REFUSAL",
38
+ "UsageError",
39
+ "cmd_launch", "cmd_status", "cmd_verify", "cmd_runs", "cmd_diff", "cmd_show",
40
+ "cmd_branch", "cmd_control", "cmd_list", "cmd_matrix", "cmd_check", "cmd_search",
41
+ "cmd_install", "cmd_replay", "cmd_dashboard", "cmd_exec_run",
42
+ "matrix",
43
+ ]
44
+
45
+ REPLAY_REFUSAL = ("MATE replay: visual replay is not available in MATE 1.0.0 (the replay "
46
+ "interface of systems is not yet specified)")
47
+ DASHBOARD_REFUSAL = ("the terminal dashboard is not available in MATE 1.0.0; control a run "
48
+ "with 'MATE control <run> stop|suspend|resume|checkpoint|verbosity'")
49
+
50
+ _FAILURES = ("method_error", "system_error", "refused")
51
+
52
+
53
+ class UsageError(Exception):
54
+ """A command-line argument the command cannot use (exit status 3)."""
55
+
56
+
57
+ def _out(text: str) -> None:
58
+ """Print a report."""
59
+ print(text, file=sys.stdout)
60
+
61
+
62
+ def _err(text: str) -> None:
63
+ """Print an error."""
64
+ print(text, file=sys.stderr)
65
+
66
+
67
+ # --- launching ---------------------------------------------------------------------------------
68
+
69
+
70
+ def _sweep(specs: Sequence[str]) -> list[tuple[str, Sequence[Value]]]:
71
+ """``--sweep path=v1,v2`` -> (path, values), each value a TOML value ([C] R21.2)."""
72
+ out = []
73
+ for spec in specs:
74
+ path, sep, values = spec.partition("=")
75
+ if not sep or not path.strip() or not values.strip():
76
+ raise UsageError(f"--sweep {spec!r}: expected <path>=<v1>,<v2>,...")
77
+ try:
78
+ parsed = parse_toml_value(f"[{values}]")
79
+ except ValueError as exc:
80
+ raise UsageError(f"--sweep {spec!r}: {exc}") from None
81
+ out.append((path.strip(), tuple(parsed))) # type: ignore[arg-type]
82
+ return out
83
+
84
+
85
+ def _overrides(args: argparse.Namespace) -> dict[str, Value]:
86
+ """The ``[execution]`` keys given on the command line ([C] R21.1)."""
87
+ out: dict[str, Value] = {}
88
+ for key in ("workers", "executor", "scheduler", "verbosity", "workdir", "retention",
89
+ "checkpoint_every"):
90
+ value = getattr(args, key, None)
91
+ if value is not None:
92
+ out[key] = str(value) if key == "workdir" else value
93
+ return out
94
+
95
+
96
+ def _sinks(args: argparse.Namespace) -> list[str] | None:
97
+ """``--sinks S,...``."""
98
+ if getattr(args, "sinks", None) is None:
99
+ return None
100
+ return [s.strip() for s in args.sinks.split(",") if s.strip()]
101
+
102
+
103
+ def _load(load: object, debug: bool = False) -> LoadedProtocol | int:
104
+ """Call a loading thunk; a refusal is printed and gives status 1.
105
+
106
+ No run directory exists yet: the software check's tracebacks go to stderr under
107
+ ``--debug``.
108
+ """
109
+ try:
110
+ return load() # type: ignore[operator]
111
+ except ProtocolRefused as exc:
112
+ _err(exc.render())
113
+ except SoftwareCompatibilityError as exc:
114
+ _err(exc.render())
115
+ traces = [f.debug for f in exc.report.failures if f.debug]
116
+ if traces:
117
+ _err("\n".join(traces) if debug else "Rerun with --debug to print the traceback.")
118
+ except ValueError as exc:
119
+ _err(str(exc))
120
+ return 1
121
+
122
+
123
+ def _launch(loaded: LoadedProtocol, args: argparse.Namespace) -> int:
124
+ """Launch and report; a submission ``sbatch`` cannot make gives status 1."""
125
+ try:
126
+ result = launch(loaded, sinks=_sinks(args), debug=args.debug, dry_run=args.dry_run)
127
+ except SubmissionError as exc:
128
+ _err(str(exc))
129
+ return 1
130
+ return _report(loaded, result, args.dry_run)
131
+
132
+
133
+ def _report(loaded: LoadedProtocol, result: LaunchResult, dry_run: bool) -> int:
134
+ """Print what a launch did; status 2 when a run failed."""
135
+ names = {row.identity: row.directory for row in loaded.runs}
136
+ for note in loaded.notes:
137
+ _out(note)
138
+ failed = 0
139
+ skipped = dict(result.skipped)
140
+ for ref, cause in result.runs.items():
141
+ if ref in skipped:
142
+ shown = f"left alone, {skipped[ref]}"
143
+ elif cause is None:
144
+ shown = "prepared" if dry_run else "submitted" if result.submitted else "not run"
145
+ else:
146
+ shown = cause.kind + (f" ({cause.detail})" if cause.detail else "")
147
+ if cause.generation is not None:
148
+ shown += f" at generation {cause.generation}"
149
+ failed += cause.kind in _FAILURES
150
+ _out(f" {names.get(ref, ref.fingerprint[:12]):<10} {shown}")
151
+ for _, message in result.missing:
152
+ _out(f" {message}")
153
+ if result.submitted:
154
+ _out(f"submitted Slurm job(s): {', '.join(result.submitted)}")
155
+ _out(f"{len(result.runs)} run(s) in {result.workdir}"
156
+ + (f", {failed} failed" if failed else ""))
157
+ return 2 if failed else 0
158
+
159
+
160
+ def cmd_launch(args: argparse.Namespace) -> int:
161
+ """``MATE launch <proto.toml>``: load, check, schedule ([X] §24)."""
162
+ if args.dashboard:
163
+ _err(DASHBOARD_REFUSAL)
164
+ return 1
165
+ sweep = _sweep(args.sweep or [])
166
+ loaded = _load(lambda: load_protocol(Path(args.protocol), cwd=Path.cwd(),
167
+ execution_overrides=_overrides(args), sweep=sweep),
168
+ args.debug)
169
+ if isinstance(loaded, int):
170
+ return loaded
171
+ return _launch(loaded, args)
172
+
173
+
174
+ def _branch_target(text: str) -> tuple[Path, int | str]:
175
+ """``<run>@<generation>``."""
176
+ run, sep, generation = text.rpartition("@")
177
+ if not sep or not run:
178
+ raise UsageError(f"{text!r}: expected <run directory>@<generation>")
179
+ if generation == "end":
180
+ return Path(run), "end"
181
+ try:
182
+ g = int(generation)
183
+ except ValueError:
184
+ raise UsageError(f"{text!r}: the generation is an integer or 'end'") from None
185
+ if g < 0:
186
+ raise UsageError(f"{text!r}: the generation is an integer >= 0 or 'end'")
187
+ return Path(run), g
188
+
189
+
190
+ def cmd_branch(args: argparse.Namespace) -> int:
191
+ """``MATE branch <run>@<g> --count N --root-seed S`` ([C] R21.3)."""
192
+ run, generation = _branch_target(args.target)
193
+ if args.root_seed == "fresh":
194
+ root_seed: int | str = "fresh"
195
+ else:
196
+ try:
197
+ root_seed = int(args.root_seed)
198
+ except ValueError:
199
+ raise UsageError(f"--root-seed {args.root_seed!r}: an integer or 'fresh'") from None
200
+ try:
201
+ raw = workdir.branch_protocol(run, generation, args.count, root_seed) # type: ignore[arg-type]
202
+ except workdir.WorkdirError as exc:
203
+ _err(exc.message)
204
+ return exc.exit_status
205
+ overrides = _overrides(args)
206
+ if "workdir" not in overrides:
207
+ overrides["workdir"] = str(Path.cwd() / raw["protocol"]["name"])
208
+ loaded = _load(lambda: load_protocol(raw, cwd=Path.cwd(), execution_overrides=overrides),
209
+ args.debug)
210
+ if isinstance(loaded, int):
211
+ return loaded
212
+ return _launch(loaded, args)
213
+
214
+
215
+ def cmd_exec_run(args: argparse.Namespace) -> int:
216
+ """Hidden ``MATE exec-run <run directory>`` ([X] SC4); a run left alone gives status 0."""
217
+ try:
218
+ return exec_run(Path(args.directory), debug=args.debug)
219
+ except RunBusyError as exc:
220
+ _out(exc.message)
221
+ return 0
222
+
223
+
224
+ # --- the workdir -------------------------------------------------------------------------------
225
+
226
+
227
+ def _workdir_call(call: object) -> int:
228
+ """Print what a workdir reader returns; its errors carry their status."""
229
+ try:
230
+ text = call() # type: ignore[operator]
231
+ except workdir.WorkdirError as exc:
232
+ _err(exc.message)
233
+ return exc.exit_status
234
+ _out(text)
235
+ return 0
236
+
237
+
238
+ def cmd_status(args: argparse.Namespace) -> int:
239
+ """``MATE status <workdir>`` ([C] R21.4)."""
240
+ return _workdir_call(lambda: workdir.status(Path(args.workdir), args.protocol))
241
+
242
+
243
+ def cmd_verify(args: argparse.Namespace) -> int:
244
+ """``MATE verify <workdir>`` ([C] R19.8): non-zero on any violation."""
245
+ try:
246
+ text, ok = workdir.verify_workdir(Path(args.workdir), args.protocol)
247
+ except workdir.WorkdirError as exc:
248
+ _err(exc.message)
249
+ return exc.exit_status
250
+ (_out if ok else _err)(text)
251
+ return 0 if ok else 1
252
+
253
+
254
+ def cmd_runs(args: argparse.Namespace) -> int:
255
+ """``MATE runs <workdir>`` ([C] R21.4)."""
256
+ return _workdir_call(lambda: workdir.runs(Path(args.workdir), args.protocol))
257
+
258
+
259
+ def cmd_diff(args: argparse.Namespace) -> int:
260
+ """``MATE diff <a> <b>`` ([C] R21.5)."""
261
+ return _workdir_call(lambda: workdir.diff(Path(args.a), Path(args.b)))
262
+
263
+
264
+ def cmd_show(args: argparse.Namespace) -> int:
265
+ """``MATE show <run or workdir>`` (design §1.2)."""
266
+ return _workdir_call(lambda: workdir.show(Path(args.path), args.objective, args.subpop))
267
+
268
+
269
+ def cmd_control(args: argparse.Namespace) -> int:
270
+ """``MATE control <run> <command> [value]`` (DD-3)."""
271
+ directory = Path(args.run)
272
+ if not (directory / "plan.json").is_file():
273
+ raise UsageError(f"{directory}: not a run directory (no plan.json)")
274
+ if args.command_name == "verbosity" and args.value is None:
275
+ raise UsageError("control verbosity: a value is required")
276
+ try:
277
+ seq = send_command(directory, args.command_name, args.value)
278
+ except ValueError as exc:
279
+ raise UsageError(str(exc)) from None
280
+ _out(f"command {seq} ({args.command_name}) sent to {directory}")
281
+ return 0
282
+
283
+
284
+ # --- modules -----------------------------------------------------------------------------------
285
+
286
+
287
+ def _modules() -> list[ModuleEntry]:
288
+ """Installed modules, one per (kind, name)."""
289
+ seen: dict[tuple[str, str], ModuleEntry] = {}
290
+ for e in installed_modules():
291
+ seen.setdefault((e.kind, e.name), e)
292
+ return list(seen.values())
293
+
294
+
295
+ def cmd_list(args: argparse.Namespace) -> int:
296
+ """``MATE list``: installed modules and their software status ([X] S10)."""
297
+ entries = _modules()
298
+ if not entries:
299
+ _out("no module installed")
300
+ return 0
301
+ for e in entries:
302
+ report = check_software([ModuleRef(e.kind, e.name)], None)
303
+ if report.ok:
304
+ status = "ok"
305
+ else:
306
+ f = report.failures[0]
307
+ status = f"failed: requires {f.requirement}; found {f.found}"
308
+ _out(f"{e.kind:<7} {e.name:<24} {e.distribution} {e.distribution_version:<10} "
309
+ f"{status}")
310
+ return 0
311
+
312
+
313
+ class _Module:
314
+ """A module as ``MATE matrix`` sees it: constructed on its declared defaults, or why not."""
315
+
316
+ def __init__(self, entry: ModuleEntry) -> None:
317
+ self.entry = entry
318
+ self.instance: object | None = None
319
+ self.specs: NormalisedSpecs | None = None
320
+ self.note = ""
321
+ report = check_software([ModuleRef(entry.kind, entry.name)], None)
322
+ if not report.ok:
323
+ f = report.failures[0]
324
+ self.note = f"software: requires {f.requirement}; found {f.found}"
325
+ return
326
+ cls = report.classes[(entry.kind, entry.name)]
327
+ missing = missing_parameters(cls)
328
+ if missing:
329
+ self.note = f"needs settings: {', '.join(missing)}"
330
+ return
331
+ try:
332
+ self.instance = construct_module(cls, declared_defaults(cls))
333
+ except ModuleConstructionError as exc:
334
+ self.note = exc.message
335
+ return
336
+ if entry.kind == "system":
337
+ owns = bool(getattr(self.instance.capabilities, "owns_loop", False)) # type: ignore[union-attr]
338
+ specs, refusals = normalise_specs(self.instance, owns_loop=owns) # type: ignore[arg-type]
339
+ if specs is None:
340
+ self.note = "; ".join(r.message for r in refusals)
341
+ self.instance = None
342
+ return
343
+ self.specs = specs
344
+ if owns:
345
+ self.note = "owns its loop"
346
+ elif is_collective(specs):
347
+ self.note = "collective evaluation (not available in MATE 1.0.0)"
348
+
349
+ @property
350
+ def usable(self) -> bool:
351
+ """Whether cells can be computed for it."""
352
+ return self.instance is not None and not self.note
353
+
354
+
355
+ def _cell(method: _Module, system: _Module) -> tuple[str, bool]:
356
+ """``(cell text, compatible)`` of one pair ([X] MX2)."""
357
+ specs = system.specs
358
+ assert specs is not None and specs.interface is not None
359
+ report = check_interface(specs.interface, specs.performance, specs.roles,
360
+ method.instance.requirements, # type: ignore[union-attr]
361
+ system.instance.capabilities) # type: ignore[union-attr]
362
+ if report.outcome is Outcome.INCOMPATIBLE:
363
+ dims = ", ".join(dict.fromkeys(m.dimension for m in report.mismatches))
364
+ return f"incompatible ({dims})", False
365
+ if report.outcome is Outcome.CONDITIONAL:
366
+ bounds = ", ".join(dict.fromkeys(f"{c.organ}.{c.bound}" for c in report.conditional))
367
+ return f"conditional ({bounds})", True
368
+ return "compatible", True
369
+
370
+
371
+ _REQUIRED = frozenset({"P-META-MISSING", "P-SETTING-MISSING"})
372
+
373
+
374
+ def _required(refusals: Sequence[Refusal]) -> str:
375
+ """``method: a, b; system: c``: the settings a smoke protocol lacks, by module."""
376
+ groups: dict[str, list[str]] = {}
377
+ for r in refusals:
378
+ kind = "system" if r.code == "P-SETTING-MISSING" else "method"
379
+ groups.setdefault(kind, []).append(r.where.rsplit(" ", 1)[-1])
380
+ return "; ".join(f"{kind}: {', '.join(names)}" for kind, names in groups.items())
381
+
382
+
383
+ def _smoke(method: str, system: str, directory: Path) -> tuple[str, bool]:
384
+ """Two real generations of a pair ([X] MX2): ``(outcome, passed)``.
385
+
386
+ The outcome is ``ok``, the stop cause, or the settings the pair needs, declared without
387
+ a default: a protocol must write them, so the pair is not run, and that is no failure.
388
+ """
389
+ raw = {
390
+ "protocol": {"name": f"{method}-{system}"},
391
+ "system": {"module": system},
392
+ "method": {"module": method},
393
+ "replicates": {"count": 1, "root_seed": 0},
394
+ "stop": {"generations": 2},
395
+ "execution": {"executor": "serial", "retention": "minimal", "sinks": ["silent"],
396
+ "workdir": str(directory / f"{method}-{system}")},
397
+ }
398
+ try:
399
+ loaded = load_protocol(raw, cwd=directory)
400
+ except ProtocolRefused as exc:
401
+ if all(r.code in _REQUIRED for r in exc.refusals):
402
+ return f"settings required ({_required(exc.refusals)})", True
403
+ return "refused (" + ", ".join(dict.fromkeys(r.code for r in exc.refusals)) + ")", False
404
+ except SoftwareCompatibilityError:
405
+ return "refused (software)", False
406
+ try:
407
+ result = launch(loaded)
408
+ except Exception as exc: # a smoke test reports whatever breaks
409
+ return f"failed: {type(exc).__name__}: {exc}", False
410
+ causes = [c for c in result.runs.values() if c is not None]
411
+ if not causes:
412
+ return "not run", False
413
+ cause = causes[0]
414
+ if cause.kind == "criterion":
415
+ return "ok", True
416
+ return "failed: " + cause.kind + (f" ({cause.detail})" if cause.detail else ""), False
417
+
418
+
419
+ def matrix(smoke: bool = False, entries: Sequence[ModuleEntry] | None = None
420
+ ) -> tuple[list[str], bool]:
421
+ """Compute the methods x systems table of installed modules ([X] §23.1).
422
+
423
+ Parameters
424
+ ----------
425
+ smoke : bool, default False
426
+ Run two generations of every compatible pair (serial executor, temporary
427
+ directory, seed 0, retention ``minimal``).
428
+ entries : sequence of ModuleEntry, optional
429
+ The modules; default every installed module.
430
+
431
+ Returns
432
+ -------
433
+ tuple
434
+ ``(lines, ok)``: a line per module that yields no cell (with the reason), then one
435
+ line per (method, system) pair; ``ok`` is ``False`` when a smoke run failed. A pair
436
+ whose modules declare settings without a default shows them as required and does
437
+ not fail.
438
+ """
439
+ entries = _modules() if entries is None else list(entries)
440
+ modules = [_Module(e) for e in entries]
441
+ methods = [m for m in modules if m.entry.kind == "method"]
442
+ systems = [m for m in modules if m.entry.kind == "system"]
443
+ lines: list[str] = []
444
+ for m in modules:
445
+ if not m.usable:
446
+ lines.append(f"{m.entry.kind} {m.entry.name}: {m.note}")
447
+ width_m = max((len(m.entry.name) for m in methods), default=6)
448
+ width_s = max((len(s.entry.name) for s in systems), default=6)
449
+ lines.append(f"{'method':<{width_m}} {'system':<{width_s}} result")
450
+ ok = True
451
+ with tempfile.TemporaryDirectory(prefix="MATE-matrix-") as tmp:
452
+ for m in methods:
453
+ for s in systems:
454
+ if not (m.usable and s.usable):
455
+ cell = "-"
456
+ compatible = False
457
+ else:
458
+ cell, compatible = _cell(m, s)
459
+ if smoke and compatible:
460
+ outcome, passed = _smoke(m.entry.name, s.entry.name, Path(tmp))
461
+ ok = ok and passed
462
+ cell += f"; smoke: {outcome}"
463
+ lines.append(f"{m.entry.name:<{width_m}} {s.entry.name:<{width_s}} {cell}")
464
+ return lines, ok
465
+
466
+
467
+ def cmd_matrix(args: argparse.Namespace) -> int:
468
+ """``MATE matrix [--smoke]`` ([X] §23.1): status 2 when a smoke run fails."""
469
+ lines, ok = matrix(args.smoke)
470
+ _out("\n".join(lines))
471
+ return 0 if ok else 2
472
+
473
+
474
+ def _settings(pairs: Sequence[str]) -> dict[str, Value]:
475
+ """``--set k=v`` -> settings, each value a TOML value."""
476
+ out: dict[str, Value] = {}
477
+ for pair in pairs:
478
+ key, sep, text = pair.partition("=")
479
+ if not sep or not key.strip():
480
+ raise UsageError(f"--set {pair!r}: expected <key>=<value>")
481
+ try:
482
+ out[key.strip()] = parse_toml_value(text)
483
+ except ValueError as exc:
484
+ raise UsageError(f"--set {pair!r}: {exc}") from None
485
+ return out
486
+
487
+
488
+ def cmd_check(args: argparse.Namespace) -> int:
489
+ """``MATE check <module>`` ([X] §22): status 2 when a test fails (CF7)."""
490
+ # Imported here: the conformance suite is a heavy, separate part of the runtime.
491
+ from MATE.conformance import check_module
492
+
493
+ settings = _settings(args.set or [])
494
+ try:
495
+ results = check_module(args.module, kind=args.kind, system=args.system,
496
+ method=args.method,
497
+ settings=settings, tolerance=args.tolerance)
498
+ except (ProtocolRefused, SoftwareCompatibilityError) as exc:
499
+ _err(exc.render())
500
+ return 1
501
+ except ValueError as exc:
502
+ _err(str(exc))
503
+ return 1
504
+ failed = 0
505
+ for r in results:
506
+ failed += r.status == "fail"
507
+ _out(f" {r.test:<15} {r.status:<5} {r.detail}".rstrip())
508
+ _out(f"{args.module}: {len(results)} test(s), {failed} failed")
509
+ return 2 if failed else 0
510
+
511
+
512
+ def cmd_search(args: argparse.Namespace) -> int:
513
+ """``MATE search <text>`` ([X] RG5): one line per module, with the version installed by
514
+ default."""
515
+ try:
516
+ found = registry.search(args.text)
517
+ except registry.RegistryError as exc:
518
+ _err(str(exc))
519
+ return 1
520
+ for url in registry.registry_urls():
521
+ age = registry.cache_age(url)
522
+ if age is not None and age >= 86400:
523
+ _out(f"(registry {url}: cache from {age / 86400:.1f} day(s) ago, offline or not "
524
+ f"updated)")
525
+ for e in found:
526
+ latest = e.latest()
527
+ version = latest.version if latest is not None else "withdrawn"
528
+ for m in e.modules:
529
+ organs = ", ".join(f"{k}: {v}" for k, v in m.organs.items())
530
+ owner = f" ({e.distribution})" if m.name != e.name else ""
531
+ _out(f"{m.kind:<7} {m.name:<20} {version:<12} {e.summary}{owner}"
532
+ + (f" [{organs}]" if organs else ""))
533
+ if not found:
534
+ _out(f"no module matches {args.text!r}")
535
+ return 0
536
+
537
+
538
+ def cmd_install(args: argparse.Namespace) -> int:
539
+ """``MATE install <name>...`` ([X] RG6): pip, then the software check of the modules."""
540
+ status = 0
541
+ for name in args.names:
542
+ try:
543
+ done = registry.install(name)
544
+ except registry.RegistryError as exc:
545
+ _err(str(exc))
546
+ status = 1
547
+ continue
548
+ wanted = {canonicalize_name(d) for d in done.distributions}
549
+ refs = [ModuleRef(e.kind, e.name) for e in _modules()
550
+ if canonicalize_name(e.distribution) in wanted]
551
+ if not refs:
552
+ _out(f"installed {done.target}; it declares no MATE module")
553
+ continue
554
+ report = check_software(refs, None)
555
+ (_out if report.ok else _err)(render(report))
556
+ status = status or (0 if report.ok else 1)
557
+ return status
558
+
559
+
560
+ # --- deferred ----------------------------------------------------------------------------------
561
+
562
+
563
+ def cmd_replay(args: argparse.Namespace) -> int:
564
+ """``MATE replay``: refused in MATE 1.0.0 (design §1.3)."""
565
+ _err(REPLAY_REFUSAL)
566
+ return 1
567
+
568
+
569
+ def cmd_dashboard(args: argparse.Namespace) -> int:
570
+ """``MATE dashboard``: refused in MATE 1.0.0 (design §1.3)."""
571
+ _err(DASHBOARD_REFUSAL)
572
+ return 1
573
+