param_form_utils.py 7.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233
  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.support.param import Parameter
  20. from datalad.utils import (
  21. get_wrapped_class,
  22. )
  23. from . import param_widgets as pw
  24. from .param_multival_widget import MultiValueInputWidget
  25. from .active_api import (
  26. api,
  27. exclude_parameters,
  28. parameter_display_names,
  29. )
  30. from .api_utils import get_cmd_params
  31. from .utils import _NoValue
  32. from .constraints import (
  33. EnsureExistingDirectory,
  34. EnsureDatasetSiblingName,
  35. )
  36. __all__ = ['populate_form_w_params']
  37. def populate_form_w_params(
  38. basedir: Path,
  39. formlayout: QFormLayout,
  40. cmdname: str,
  41. cmdkwargs: Dict) -> None:
  42. """Populate a given QLayout with data entry widgets for a DataLad command
  43. """
  44. # localize to potentially delay heavy import
  45. from datalad import api as dlapi
  46. # get the matching callable from the DataLad API
  47. cmd = getattr(dlapi, cmdname)
  48. cmd_api_spec = api.get(cmdname, {})
  49. cmd_param_display_names = cmd_api_spec.get(
  50. 'parameter_display_names', {})
  51. # resolve to the interface class that has all the specification
  52. cmd_cls = get_wrapped_class(cmd)
  53. # collect widgets for a later connection setup
  54. form_widgets = dict()
  55. # loop over all parameters of the command (with their defaults)
  56. def _specific_params():
  57. for pname, pdefault in get_cmd_params(cmd):
  58. yield pname, pdefault, cmd_cls._params_[pname]
  59. # loop over all generic
  60. def _generic_params():
  61. for pname, param in eval_params.items():
  62. yield (
  63. pname,
  64. param.cmd_kwargs.get('default', _NoValue), \
  65. param,
  66. )
  67. for pname, pdefault, param_spec in sorted(
  68. # across cmd params, and generic params
  69. chain(_specific_params(), _generic_params()),
  70. # sort by custom order and/or parameter name
  71. key=lambda x: (
  72. cmd_api_spec.get(
  73. 'parameter_order', {}).get(x[0], 99),
  74. x[0])):
  75. if pname in exclude_parameters:
  76. continue
  77. if pname in cmd_api_spec.get('exclude_parameters', []):
  78. continue
  79. if pname in cmd_api_spec.get('parameter_constraints', []):
  80. # we have a better idea in gooey then what the original
  81. # command knows
  82. param_spec.constraints = \
  83. cmd_api_spec['parameter_constraints'][pname]
  84. # populate the layout with widgets for each of them
  85. pwidget = _get_parameter_widget(
  86. basedir,
  87. formlayout.parentWidget(),
  88. param_spec,
  89. pname,
  90. # will also be _NoValue, if there was none
  91. pdefault,
  92. )
  93. form_widgets[pname] = pwidget
  94. # query for a known display name
  95. # first in command-specific spec
  96. display_name = cmd_param_display_names.get(
  97. pname,
  98. # fallback to API specific override
  99. parameter_display_names.get(
  100. pname,
  101. # last resort:
  102. # use capitalized orginal with _ removed as default
  103. pname.replace('_', ' ').capitalize()
  104. ),
  105. )
  106. display_label = QLabel(display_name)
  107. display_label.setToolTip(f'API command parameter: `{pname}`')
  108. formlayout.addRow(display_label, pwidget)
  109. # wire widgets up to self update on changes in other widget
  110. # use case: dataset context change
  111. # so it could be just the dataset widget sending, and the other receiving.
  112. # but for now wire all with all others
  113. for pname1, pwidget1 in form_widgets.items():
  114. for pname2, pwidget2 in form_widgets.items():
  115. if pname1 == pname2:
  116. continue
  117. pwidget1.value_changed.connect(
  118. pwidget2.init_gooey_from_other_param)
  119. # when all is wired up, set the values that need setting
  120. for pname, pwidget in form_widgets.items():
  121. if pname in cmdkwargs:
  122. pwidget.set_gooey_param_value(cmdkwargs[pname])
  123. #
  124. # Internal helpers
  125. #
  126. def _get_parameter_widget(
  127. basedir: Path,
  128. parent: QWidget,
  129. param: Parameter,
  130. name: str,
  131. default: Any = pw._NoValue) -> QWidget:
  132. """Populate a given layout with a data entry widget for a command parameter
  133. `value` is an explicit setting requested by the caller. A value of
  134. `_NoValue` indicates that there was no specific value given. `default` is a
  135. command's default parameter value, with `_NoValue` indicating that the
  136. command has no default for a parameter.
  137. """
  138. # guess the best widget-type based on the argparse setup and configured
  139. # constraints
  140. pwid_factory = _get_parameter_widget_factory(
  141. name,
  142. default,
  143. param.constraints,
  144. param.cmd_kwargs,
  145. basedir)
  146. return pw.load_parameter_widget(
  147. parent,
  148. pwid_factory,
  149. name=name,
  150. docs=param._doc,
  151. default=default,
  152. validator=param.constraints,
  153. )
  154. def _get_parameter_widget_factory(
  155. name: str,
  156. default: Any,
  157. constraints: Callable or None,
  158. argparse_spec: Dict,
  159. basedir: Path) -> Callable:
  160. """Translate DataLad command parameter specs into Gooey input widgets"""
  161. # for now just one to play with
  162. # TODO each factory must provide a standard widget method
  163. # to return the final value, ready to pass onto the respective
  164. # parameter of the command call
  165. argparse_action = argparse_spec.get('action')
  166. # we must consider the following action specs for widget selection
  167. # - 'store_const'
  168. # - 'store_true' and 'store_false'
  169. # - 'append'
  170. # - 'append_const'
  171. # - 'count'
  172. # - 'extend'
  173. #if name == 'path':
  174. # return get_pathselection_widget
  175. # if we have no idea, use a simple line edit
  176. type_widget = pw.StrParamWidget
  177. # now some parameters where we can derive semantics from their name
  178. if name == 'dataset' or isinstance(constraints, EnsureExistingDirectory):
  179. type_widget = functools.partial(
  180. pw.PathParamWidget,
  181. pathtype=QFileDialog.Directory,
  182. basedir=basedir)
  183. elif name == 'path':
  184. type_widget = functools.partial(
  185. pw.PathParamWidget, basedir=basedir)
  186. elif name == 'cfg_proc':
  187. type_widget = pw.CfgProcParamWidget
  188. elif name == 'recursion_limit':
  189. type_widget = functools.partial(pw.PosIntParamWidget, allow_none=True)
  190. # now parameters where we make decisions based on their configuration
  191. elif isinstance(constraints, EnsureDatasetSiblingName):
  192. type_widget = pw.SiblingChoiceParamWidget
  193. elif argparse_action in ('store_true', 'store_false'):
  194. type_widget = pw.BoolParamWidget
  195. elif isinstance(constraints, EnsureChoice) and argparse_action is None:
  196. type_widget = functools.partial(
  197. pw.ChoiceParamWidget, choices=constraints._allowed)
  198. elif argparse_spec.get('choices'):
  199. type_widget = functools.partial(
  200. pw.ChoiceParamWidget, choices=argparse_spec.get('choices'))
  201. # we must consider the following nargs spec for widget selection
  202. # (int, '*', '+'), plus action=append
  203. # in all these cases, we need to expect multiple instances of the data type
  204. # for which we have selected the input widget above
  205. argparse_nargs = argparse_spec.get('nargs')
  206. if (argparse_action == 'append'
  207. or argparse_nargs in ('+', '*')
  208. or isinstance(argparse_nargs, int)):
  209. type_widget = functools.partial(
  210. # TODO give a fixed N as a parameter too
  211. MultiValueInputWidget, type_widget)
  212. return type_widget