param_form_utils.py 7.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201
  1. from collections.abc import Callable
  2. import functools
  3. from itertools import zip_longest
  4. from typing import (
  5. Any,
  6. Dict,
  7. List,
  8. )
  9. from PySide6.QtWidgets import (
  10. QFormLayout,
  11. QLabel,
  12. QWidget,
  13. QFileDialog,
  14. )
  15. from datalad.interface.base import Interface
  16. from datalad.interface.common_opts import eval_params
  17. from datalad.support.constraints import EnsureChoice
  18. from datalad.support.param import Parameter
  19. from datalad.utils import (
  20. getargspec,
  21. get_wrapped_class,
  22. )
  23. from . import param_widgets as pw
  24. from .param_multival_widget import MultiValueInputWidget
  25. __all__ = ['populate_form_w_params']
  26. def populate_form_w_params(
  27. formlayout: QFormLayout,
  28. cmdname: str,
  29. cmdkwargs: Dict) -> None:
  30. """Populate a given QLayout with data entry widgets for a DataLad command
  31. """
  32. # localize to potentially delay heavy import
  33. from datalad import api as dlapi
  34. # deposit the command name in the widget, to be retrieved later by
  35. # retrieve_parameters()
  36. formlayout.datalad_cmd_name = cmdname
  37. # get the matching callable from the DataLad API
  38. cmd = getattr(dlapi, cmdname)
  39. # resolve to the interface class that has all the specification
  40. cmd_cls = get_wrapped_class(cmd)
  41. # loop over all parameters of the command (with their defaults)
  42. for pname, pdefault in _get_params(cmd):
  43. # populate the layout with widgets for each of them
  44. pwidget = _get_parameter_widget(
  45. formlayout.parentWidget(),
  46. cmd_cls._params_[pname],
  47. pname,
  48. # pass a given value, or indicate that there was none
  49. cmdkwargs.get(pname, pw._NoValue),
  50. # will also be _NoValue, if there was none
  51. pdefault,
  52. # pass the full argspec too, to make it possible for
  53. # some widget to act clever based on other parameter
  54. # settings that are already known at setup stage
  55. # (e.g. setting the base dir of a file selector based
  56. # on a `dataset` argument)
  57. allargs=cmdkwargs,
  58. )
  59. formlayout.addRow(pname, pwidget)
  60. # Add widgets for standard options like result_renderer:
  61. for pname, param in eval_params.items():
  62. if pname not in ['result_renderer', 'on_failure']:
  63. continue
  64. pwidget = _get_parameter_widget(
  65. formlayout.parentWidget(),
  66. param,
  67. pname,
  68. # pass a given value, or indicate that there was none
  69. cmdkwargs.get(pname, pw._NoValue),
  70. # will also be _NoValue, if there was none
  71. param.cmd_kwargs.get('default', pw._NoValue),
  72. # The generic options are not command specific and have no relation
  73. # to command specific ones. Hence, allargs shouldn't be needed.
  74. allargs=None,
  75. )
  76. formlayout.addRow(pname, pwidget)
  77. #
  78. # Internal helpers
  79. #
  80. def _get_params(cmd) -> List:
  81. """Take a callable and return a list of parameter names, and their defaults
  82. Parameter names and defaults are returned as 2-tuples. If a parameter has
  83. no default, the special value `_NoValue` is used.
  84. """
  85. # lifted from setup_parser_for_interface()
  86. args, varargs, varkw, defaults = getargspec(cmd, include_kwonlyargs=True)
  87. return list(
  88. zip_longest(
  89. # fuse parameters from the back, to match with their respective
  90. # defaults -- if soem have no defaults, they would be the first
  91. args[::-1],
  92. defaults[::-1],
  93. # pad with a dedicate type, to be able to tell if there was a
  94. # default or not
  95. fillvalue=pw._NoValue)
  96. # reverse the order again to match the original order in the signature
  97. )[::-1]
  98. def _get_parameter_widget(
  99. parent: QWidget,
  100. param: Parameter,
  101. name: str,
  102. value: Any = pw._NoValue,
  103. default: Any = pw._NoValue,
  104. allargs: Dict or None = None) -> QWidget:
  105. """Populate a given layout with a data entry widget for a command parameter
  106. `value` is an explicit setting requested by the caller. A value of
  107. `_NoValue` indicates that there was no specific value given. `default` is a
  108. command's default parameter value, with `_NoValue` indicating that the
  109. command has no default for a parameter.
  110. """
  111. # guess the best widget-type based on the argparse setup and configured
  112. # constraints
  113. pwid_factory = _get_parameter_widget_factory(
  114. name, default, param.constraints, param.cmd_kwargs,
  115. allargs if allargs else dict())
  116. return pw.load_parameter_widget(
  117. parent,
  118. pwid_factory,
  119. name=name,
  120. docs=param._doc,
  121. value=value,
  122. default=default,
  123. validator=param.constraints,
  124. allargs=allargs,
  125. )
  126. def _get_parameter_widget_factory(
  127. name: str,
  128. default: Any,
  129. constraints: Callable or None,
  130. argparse_spec: Dict,
  131. allargs: Dict) -> Callable:
  132. """Translate DataLad command parameter specs into Gooey input widgets"""
  133. # for now just one to play with
  134. # TODO each factory must provide a standard widget method
  135. # to return the final value, ready to pass onto the respective
  136. # parameter of the command call
  137. argparse_action = argparse_spec.get('action')
  138. # we must consider the following action specs for widget selection
  139. # - 'store_const'
  140. # - 'store_true' and 'store_false'
  141. # - 'append'
  142. # - 'append_const'
  143. # - 'count'
  144. # - 'extend'
  145. #if name == 'path':
  146. # return get_pathselection_widget
  147. dspath = allargs.get('dataset')
  148. if hasattr(dspath, 'pathobj'):
  149. # matches a dataset/repo instance
  150. dspath = dspath.pathobj
  151. # if we have no idea, use a simple line edit
  152. type_widget = pw.StrParamWidget
  153. if name == 'dataset':
  154. type_widget = functools.partial(
  155. pw.PathParamWidget, pathtype=QFileDialog.Directory)
  156. if name == 'path':
  157. type_widget = functools.partial(
  158. pw.PathParamWidget, basedir=dspath)
  159. if argparse_action in ('store_true', 'store_false'):
  160. type_widget = pw.BoolParamWidget
  161. elif isinstance(constraints, EnsureChoice) and argparse_action is None:
  162. type_widget = functools.partial(
  163. pw.ChoiceParamWidget, choices=constraints._allowed)
  164. elif argparse_spec.get('choices'):
  165. type_widget = functools.partial(
  166. pw.ChoiceParamWidget, choices=argparse_spec.get('choices'))
  167. elif name == 'recursion_limit':
  168. type_widget = functools.partial(pw.PosIntParamWidget, allow_none=True)
  169. # we must consider the following nargs spec for widget selection
  170. # (int, '*', '+'), plus action=append
  171. # in all these cases, we need to expect multiple instances of the data type
  172. # for which we have selected the input widget above
  173. argparse_nargs = argparse_spec.get('nargs')
  174. if (argparse_action == 'append'
  175. or argparse_nargs in ('+', '*')
  176. or isinstance(argparse_nargs, int)):
  177. type_widget = functools.partial(
  178. # TODO give a fixed N as a parameter too
  179. MultiValueInputWidget, type_widget)
  180. return type_widget