param_form_utils.py 9.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273
  1. from collections.abc import Callable
  2. import functools
  3. from itertools import (
  4. chain,
  5. )
  6. from pathlib import Path
  7. from typing import (
  8. Any,
  9. Dict,
  10. )
  11. from PySide6.QtWidgets import (
  12. QFormLayout,
  13. QLabel,
  14. QWidget,
  15. QFileDialog,
  16. )
  17. from datalad.interface.common_opts import eval_params
  18. from datalad.support.constraints import EnsureChoice
  19. from datalad.utils import (
  20. get_wrapped_class,
  21. )
  22. from . import param_widgets as pw
  23. from .param_multival_widget import MultiValueInputWidget
  24. from .active_suite import spec as active_suite
  25. from .api_utils import get_cmd_params
  26. from .utils import _NoValue
  27. from .constraints import (
  28. Constraint,
  29. EnsureExistingDirectory,
  30. EnsureDatasetSiblingName,
  31. )
  32. __all__ = ['populate_form_w_params']
  33. def populate_form_w_params(
  34. api,
  35. basedir: Path,
  36. formlayout: QFormLayout,
  37. cmdname: str,
  38. cmdkwargs: Dict) -> None:
  39. """Populate a given QLayout with data entry widgets for a DataLad command
  40. """
  41. # localize to potentially delay heavy import
  42. from datalad import api as dlapi
  43. # get the matching callable from the DataLad API
  44. cmd = getattr(dlapi, cmdname)
  45. cmd_api_spec = api.get(cmdname, {})
  46. cmd_param_display_names = cmd_api_spec.get(
  47. 'parameter_display_names', {})
  48. # resolve to the interface class that has all the specification
  49. cmd_cls = get_wrapped_class(cmd)
  50. # collect widgets for a later connection setup
  51. form_widgets = dict()
  52. def _get_nargs(pname, argparse_spec):
  53. # TODO we must consider the following action specs for widget selection
  54. # - 'store_const'
  55. # - 'store_true' and 'store_false'
  56. # - 'append'
  57. # - 'append_const'
  58. # - 'count'
  59. # - 'extend'
  60. if pname in cmd_api_spec.get('parameter_nargs', []):
  61. # take as gospel
  62. return cmd_api_spec['parameter_nargs'][pname]
  63. elif argparse_spec.get('action') == 'append':
  64. return '*'
  65. else:
  66. nargs = argparse_spec.get('nargs', None)
  67. try:
  68. nargs = int(nargs)
  69. except (ValueError, TypeError):
  70. pass
  71. return nargs
  72. # loop over all parameters of the command (with their defaults)
  73. def _specific_params():
  74. for pname, pdefault in get_cmd_params(cmd):
  75. yield pname, pdefault, cmd_cls._params_[pname]
  76. # loop over all generic
  77. def _generic_params():
  78. for pname, param in eval_params.items():
  79. yield (
  80. pname,
  81. param.cmd_kwargs.get('default', _NoValue), \
  82. param,
  83. )
  84. cmdkwargs_defaults = dict()
  85. for pname, pdefault, param_spec in sorted(
  86. # across cmd params, and generic params
  87. chain(_specific_params(), _generic_params()),
  88. # sort by custom order and/or parameter name
  89. key=lambda x: (
  90. cmd_api_spec.get(
  91. 'parameter_order', {}).get(x[0], 99),
  92. x[0])):
  93. if pname in active_suite.get('exclude_parameters', []):
  94. continue
  95. if pname in cmd_api_spec.get('exclude_parameters', []):
  96. continue
  97. if pname in cmd_api_spec.get('parameter_constraints', []):
  98. # we have a better idea in gooey then what the original
  99. # command knows
  100. param_spec.constraints = \
  101. cmd_api_spec['parameter_constraints'][pname]
  102. cmdkwargs_defaults[pname] = pdefault
  103. # populate the layout with widgets for each of them
  104. # we do not pass Parameter instances further down, but disassemble
  105. # and homogenize here
  106. pwidget = _get_parameter_widget(
  107. basedir=basedir,
  108. parent=formlayout.parentWidget(),
  109. name=pname,
  110. constraints=cmd_api_spec['parameter_constraints'][pname]
  111. if pname in cmd_api_spec.get('parameter_constraints', [])
  112. else param_spec.constraints,
  113. nargs=_get_nargs(pname, param_spec.cmd_kwargs),
  114. # will also be _NoValue, if there was none
  115. default=pdefault,
  116. docs=param_spec._doc,
  117. # TODO make obsolete
  118. argparse_spec=param_spec.cmd_kwargs,
  119. )
  120. form_widgets[pname] = pwidget
  121. # query for a known display name
  122. # first in command-specific spec
  123. display_name = cmd_param_display_names.get(
  124. pname,
  125. # fallback to API specific override
  126. active_suite.get('parameter_display_names', {}).get(
  127. pname,
  128. # last resort:
  129. # use capitalized orginal with _ removed as default
  130. pname.replace('_', ' ').capitalize()
  131. ),
  132. )
  133. display_label = QLabel(display_name)
  134. display_label.setToolTip(f'API command parameter: `{pname}`')
  135. formlayout.addRow(display_label, pwidget)
  136. # wire widgets up to self update on changes in other widget
  137. # use case: dataset context change
  138. # so it could be just the dataset widget sending, and the other receiving.
  139. # but for now wire all with all others
  140. for pname1, pwidget1 in form_widgets.items():
  141. for pname2, pwidget2 in form_widgets.items():
  142. if pname1 == pname2:
  143. continue
  144. pwidget1.value_changed.connect(
  145. pwidget2.init_gooey_from_params)
  146. # when all is wired up, set the values that need setting
  147. # we set the respective default value to all widgets, and
  148. # update it with the given value, if there was any
  149. # (the true command parameter default was already set above)
  150. cmdkwargs_defaults.update(cmdkwargs)
  151. for pname, pwidget in form_widgets.items():
  152. pwidget.init_gooey_from_params(cmdkwargs_defaults)
  153. #
  154. # Internal helpers
  155. #
  156. def _get_parameter_widget(
  157. basedir: Path,
  158. parent: QWidget,
  159. name: str,
  160. constraints: Constraint,
  161. nargs: int or str,
  162. default: Any = pw._NoValue,
  163. docs: str = '',
  164. argparse_spec: Dict = None) -> QWidget:
  165. """Populate a given layout with a data entry widget for a command parameter
  166. `value` is an explicit setting requested by the caller. A value of
  167. `_NoValue` indicates that there was no specific value given. `default` is a
  168. command's default parameter value, with `_NoValue` indicating that the
  169. command has no default for a parameter.
  170. """
  171. # guess the best widget-type based on the argparse setup and configured
  172. # constraints
  173. pwid_factory = _get_parameter_widget_factory(
  174. name,
  175. default,
  176. constraints,
  177. nargs,
  178. basedir,
  179. # TODO make obsolete
  180. argparse_spec)
  181. return pw.load_parameter_widget(
  182. parent,
  183. pwid_factory,
  184. name=name,
  185. docs=docs,
  186. default=default,
  187. validator=constraints,
  188. )
  189. def _get_parameter_widget_factory(
  190. name: str,
  191. default: Any,
  192. constraints: Callable or None,
  193. nargs: int or str,
  194. basedir: Path,
  195. # TODO make obsolete
  196. argparse_spec: Dict) -> Callable:
  197. """Translate DataLad command parameter specs into Gooey input widgets"""
  198. if argparse_spec is None:
  199. argparse_spec = {}
  200. argparse_action = argparse_spec.get('action')
  201. disable_manual_path_input = active_suite.get('options', {}).get(
  202. 'disable_manual_path_input', False)
  203. # if we have no idea, use a simple line edit
  204. type_widget = pw.StrParamWidget
  205. # now some parameters where we can derive semantics from their name
  206. if name == 'dataset' or isinstance(constraints, EnsureExistingDirectory):
  207. type_widget = functools.partial(
  208. pw.PathParamWidget,
  209. pathtype=QFileDialog.Directory,
  210. disable_manual_edit=disable_manual_path_input,
  211. basedir=basedir)
  212. elif name == 'path':
  213. type_widget = functools.partial(
  214. pw.PathParamWidget,
  215. disable_manual_edit=disable_manual_path_input,
  216. basedir=basedir)
  217. elif name == 'cfg_proc':
  218. type_widget = pw.CfgProcParamWidget
  219. elif name == 'credential':
  220. type_widget = pw.CredentialChoiceParamWidget
  221. elif name == 'recursion_limit':
  222. type_widget = functools.partial(pw.PosIntParamWidget, allow_none=True)
  223. # now parameters where we make decisions based on their configuration
  224. elif isinstance(constraints, EnsureDatasetSiblingName):
  225. type_widget = pw.SiblingChoiceParamWidget
  226. # TODO ideally the suite API would normalize this to a EnsureBool
  227. # constraint
  228. elif argparse_action in ('store_true', 'store_false'):
  229. type_widget = pw.BoolParamWidget
  230. elif isinstance(constraints, EnsureChoice) and argparse_action is None:
  231. type_widget = functools.partial(
  232. pw.ChoiceParamWidget, choices=constraints._allowed)
  233. # TODO ideally the suite API would normalize this to a EnsureChoice
  234. # constraint
  235. elif argparse_spec.get('choices'):
  236. type_widget = functools.partial(
  237. pw.ChoiceParamWidget, choices=argparse_spec.get('choices'))
  238. # we must consider the following nargs spec for widget selection
  239. # (int, '*', '+'), plus action=append
  240. # in all these cases, we need to expect multiple instances of the data type
  241. # for which we have selected the input widget above
  242. if isinstance(nargs, int):
  243. # we have a concrete number
  244. if nargs > 1:
  245. type_widget = functools.partial(
  246. # TODO give a fixed N as a parameter too
  247. MultiValueInputWidget, type_widget)
  248. else:
  249. if nargs in ('+', '*') or argparse_action == 'append':
  250. type_widget = functools.partial(
  251. MultiValueInputWidget, type_widget)
  252. return type_widget