mirror of https://github.com/m-labs/artiq.git
Allow experiments to specify a custom argument editor UI (#1916)
On the master/EnvExperiment side, the only addition is an optional property `argument_ui` that is made accessible to the dashboard, e.g. class Example(EnvExperiment): argument_ui = "ndscan" def build(self): … Clients – primarily artiq_dashboard, but in principle e.g. a command-line UI could do the same – can then compare the value to a list of well-known names and prefer any matching custom UI handlers. On the dashboard side, this commit adds the mechanism to register a custom argument editor for a given argument_ui string, i.e. the widget that displays the parameter values within the wider experiment UI shell with the submit button, pipeline parameters, and so on. The registry remains empty by default and would be filled by out-of-tree plugins such as ndscan. The UI state readback is implemented somewhat defensively to avoid needless disruptions to users when upgrading.
This commit is contained in:
parent
dbc87f08ff
commit
32db6ff978
|
@ -164,7 +164,7 @@ class _ArgumentEditor(QtWidgets.QTreeWidget):
|
|||
|
||||
async def _recompute_argument(self, name):
|
||||
try:
|
||||
expdesc = await self.manager.compute_expdesc(self.expurl)
|
||||
expdesc, _ = await self.manager.compute_expdesc(self.expurl)
|
||||
except:
|
||||
logger.error("Could not recompute argument '%s' of '%s'",
|
||||
name, self.expurl, exc_info=True)
|
||||
|
@ -250,7 +250,8 @@ class _ExperimentDock(QtWidgets.QMdiSubWindow):
|
|||
self.manager = manager
|
||||
self.expurl = expurl
|
||||
|
||||
self.argeditor = _ArgumentEditor(self.manager, self, self.expurl)
|
||||
editor_class = self.manager.get_argument_editor_class(expurl)
|
||||
self.argeditor = editor_class(self.manager, self, self.expurl)
|
||||
self.layout.addWidget(self.argeditor, 0, 0, 1, 5)
|
||||
self.layout.setRowStretch(0, 1)
|
||||
|
||||
|
@ -401,7 +402,7 @@ class _ExperimentDock(QtWidgets.QMdiSubWindow):
|
|||
|
||||
async def _recompute_arguments_task(self, overrides=dict()):
|
||||
try:
|
||||
expdesc = await self.manager.compute_expdesc(self.expurl)
|
||||
expdesc, ui_name = await self.manager.compute_expdesc(self.expurl)
|
||||
except:
|
||||
logger.error("Could not recompute experiment description of '%s'",
|
||||
self.expurl, exc_info=True)
|
||||
|
@ -414,12 +415,13 @@ class _ExperimentDock(QtWidgets.QMdiSubWindow):
|
|||
arginfo[k][0]["default"].insert(0, v)
|
||||
else:
|
||||
arginfo[k][0]["default"] = v
|
||||
self.manager.initialize_submission_arguments(self.expurl, arginfo)
|
||||
self.manager.initialize_submission_arguments(self.expurl, arginfo, ui_name)
|
||||
|
||||
argeditor_state = self.argeditor.save_state()
|
||||
self.argeditor.deleteLater()
|
||||
|
||||
self.argeditor = _ArgumentEditor(self.manager, self, self.expurl)
|
||||
editor_class = self.manager.get_argument_editor_class(self.expurl)
|
||||
self.argeditor = editor_class(self.manager, self, self.expurl)
|
||||
self.argeditor.restore_state(argeditor_state)
|
||||
self.layout.addWidget(self.argeditor, 0, 0, 1, 5)
|
||||
|
||||
|
@ -432,7 +434,7 @@ class _ExperimentDock(QtWidgets.QMdiSubWindow):
|
|||
|
||||
async def _recompute_sched_options_task(self):
|
||||
try:
|
||||
expdesc = await self.manager.compute_expdesc(self.expurl)
|
||||
expdesc, _ = await self.manager.compute_expdesc(self.expurl)
|
||||
except:
|
||||
logger.error("Could not recompute experiment description of '%s'",
|
||||
self.expurl, exc_info=True)
|
||||
|
@ -555,6 +557,12 @@ class _QuickOpenDialog(QtWidgets.QDialog):
|
|||
|
||||
|
||||
class ExperimentManager:
|
||||
#: Global registry for custom argument editor classes, indexed by the experiment
|
||||
#: `argument_ui` string; can be populated by dashboard plugins such as ndscan.
|
||||
#: If no handler for a requested UI name is found, the default built-in argument
|
||||
#: editor will be used.
|
||||
argument_ui_classes = dict()
|
||||
|
||||
def __init__(self, main_window, dataset_sub,
|
||||
explist_sub, schedule_sub,
|
||||
schedule_ctl, experiment_db_ctl):
|
||||
|
@ -566,6 +574,7 @@ class ExperimentManager:
|
|||
self.submission_scheduling = dict()
|
||||
self.submission_options = dict()
|
||||
self.submission_arguments = dict()
|
||||
self.argument_ui_names = dict()
|
||||
|
||||
self.datasets = dict()
|
||||
dataset_sub.add_setmodel_callback(self.set_dataset_model)
|
||||
|
@ -602,6 +611,17 @@ class ExperimentManager:
|
|||
else:
|
||||
raise ValueError("Malformed experiment URL")
|
||||
|
||||
def get_argument_editor_class(self, expurl):
|
||||
ui_name = self.argument_ui_names.get(expurl, None)
|
||||
if not ui_name and expurl[:5] == "repo:":
|
||||
ui_name = self.explist.get(expurl[5:], {}).get("argument_ui", None)
|
||||
if ui_name:
|
||||
result = self.argument_ui_classes.get(ui_name, None)
|
||||
if result:
|
||||
return result
|
||||
logger.warning("Ignoring unknown argument UI '%s'", ui_name)
|
||||
return _ArgumentEditor
|
||||
|
||||
def get_submission_scheduling(self, expurl):
|
||||
if expurl in self.submission_scheduling:
|
||||
return self.submission_scheduling[expurl]
|
||||
|
@ -631,7 +651,7 @@ class ExperimentManager:
|
|||
self.submission_options[expurl] = options
|
||||
return options
|
||||
|
||||
def initialize_submission_arguments(self, expurl, arginfo):
|
||||
def initialize_submission_arguments(self, expurl, arginfo, ui_name):
|
||||
arguments = OrderedDict()
|
||||
for name, (procdesc, group, tooltip) in arginfo.items():
|
||||
state = procdesc_to_entry(procdesc).default_state(procdesc)
|
||||
|
@ -642,6 +662,7 @@ class ExperimentManager:
|
|||
"state": state, # mutated by entries
|
||||
}
|
||||
self.submission_arguments[expurl] = arguments
|
||||
self.argument_ui_names[expurl] = ui_name
|
||||
return arguments
|
||||
|
||||
def get_submission_arguments(self, expurl):
|
||||
|
@ -651,9 +672,9 @@ class ExperimentManager:
|
|||
if expurl[:5] != "repo:":
|
||||
raise ValueError("Submission arguments must be preinitialized "
|
||||
"when not using repository")
|
||||
arginfo = self.explist[expurl[5:]]["arginfo"]
|
||||
arguments = self.initialize_submission_arguments(expurl, arginfo)
|
||||
return arguments
|
||||
class_desc = self.explist[expurl[5:]]
|
||||
return self.initialize_submission_arguments(expurl,
|
||||
class_desc["arginfo"], class_desc.get("argument_ui", None))
|
||||
|
||||
def open_experiment(self, expurl):
|
||||
if expurl in self.open_experiments:
|
||||
|
@ -755,13 +776,15 @@ class ExperimentManager:
|
|||
revision = None
|
||||
description = await self.experiment_db_ctl.examine(
|
||||
file, use_repository, revision)
|
||||
return description[class_name]
|
||||
class_desc = description[class_name]
|
||||
return class_desc, class_desc.get("argument_ui", None)
|
||||
|
||||
async def open_file(self, file):
|
||||
description = await self.experiment_db_ctl.examine(file, False)
|
||||
for class_name, class_desc in description.items():
|
||||
expurl = "file:{}@{}".format(class_name, file)
|
||||
self.initialize_submission_arguments(expurl, class_desc["arginfo"])
|
||||
self.initialize_submission_arguments(expurl, class_desc["arginfo"],
|
||||
class_desc.get("argument_ui", None))
|
||||
if expurl in self.open_experiments:
|
||||
self.open_experiments[expurl].close()
|
||||
self.open_experiment(expurl)
|
||||
|
@ -774,6 +797,7 @@ class ExperimentManager:
|
|||
"options": self.submission_options,
|
||||
"arguments": self.submission_arguments,
|
||||
"docks": self.dock_states,
|
||||
"argument_uis": self.argument_ui_names,
|
||||
"open_docks": set(self.open_experiments.keys())
|
||||
}
|
||||
|
||||
|
@ -784,6 +808,7 @@ class ExperimentManager:
|
|||
self.submission_scheduling = state["scheduling"]
|
||||
self.submission_options = state["options"]
|
||||
self.submission_arguments = state["arguments"]
|
||||
self.argument_ui_names = state.get("argument_uis", {})
|
||||
for expurl in state["open_docks"]:
|
||||
self.open_experiment(expurl)
|
||||
|
||||
|
|
|
@ -30,7 +30,6 @@ class _RepoScanner:
|
|||
raise
|
||||
for class_name, class_desc in description.items():
|
||||
name = class_desc["name"]
|
||||
arginfo = class_desc["arginfo"]
|
||||
if "/" in name:
|
||||
logger.warning("Character '/' is not allowed in experiment "
|
||||
"name (%s)", name)
|
||||
|
@ -47,7 +46,8 @@ class _RepoScanner:
|
|||
entry = {
|
||||
"file": filename,
|
||||
"class_name": class_name,
|
||||
"arginfo": arginfo,
|
||||
"arginfo": class_desc["arginfo"],
|
||||
"argument_ui": class_desc["argument_ui"],
|
||||
"scheduler_defaults": class_desc["scheduler_defaults"]
|
||||
}
|
||||
entry_dict[name] = entry
|
||||
|
|
|
@ -205,7 +205,10 @@ def examine(device_mgr, dataset_mgr, file):
|
|||
(k, (proc.describe(), group, tooltip))
|
||||
for k, (proc, group, tooltip) in argument_mgr.requested_args.items()
|
||||
)
|
||||
register_experiment(class_name, name, arginfo, scheduler_defaults)
|
||||
argument_ui = None
|
||||
if hasattr(exp_class, "argument_ui"):
|
||||
argument_ui = exp_class.argument_ui
|
||||
register_experiment(class_name, name, arginfo, argument_ui, scheduler_defaults)
|
||||
finally:
|
||||
new_keys = set(sys.modules.keys())
|
||||
for key in new_keys - previous_keys:
|
||||
|
|
Loading…
Reference in New Issue