123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201 |
- from collections.abc import Callable
- import functools
- from itertools import zip_longest
- from typing import (
- Any,
- Dict,
- List,
- )
- from PySide6.QtWidgets import (
- QFormLayout,
- QLabel,
- QWidget,
- QFileDialog,
- )
- from datalad.interface.base import Interface
- from datalad.interface.common_opts import eval_params
- from datalad.support.constraints import EnsureChoice
- from datalad.support.param import Parameter
- from datalad.utils import (
- getargspec,
- get_wrapped_class,
- )
- from . import param_widgets as pw
- from .param_multival_widget import MultiValueInputWidget
- __all__ = ['populate_form_w_params']
- def populate_form_w_params(
- formlayout: QFormLayout,
- cmdname: str,
- cmdkwargs: Dict) -> None:
- """Populate a given QLayout with data entry widgets for a DataLad command
- """
- # localize to potentially delay heavy import
- from datalad import api as dlapi
- # deposit the command name in the widget, to be retrieved later by
- # retrieve_parameters()
- formlayout.datalad_cmd_name = cmdname
- # get the matching callable from the DataLad API
- cmd = getattr(dlapi, cmdname)
- # resolve to the interface class that has all the specification
- cmd_cls = get_wrapped_class(cmd)
- # loop over all parameters of the command (with their defaults)
- for pname, pdefault in _get_params(cmd):
- # populate the layout with widgets for each of them
- pwidget = _get_parameter_widget(
- formlayout.parentWidget(),
- cmd_cls._params_[pname],
- pname,
- # pass a given value, or indicate that there was none
- cmdkwargs.get(pname, pw._NoValue),
- # will also be _NoValue, if there was none
- pdefault,
- # pass the full argspec too, to make it possible for
- # some widget to act clever based on other parameter
- # settings that are already known at setup stage
- # (e.g. setting the base dir of a file selector based
- # on a `dataset` argument)
- allargs=cmdkwargs,
- )
- formlayout.addRow(pname, pwidget)
- # Add widgets for standard options like result_renderer:
- for pname, param in eval_params.items():
- if pname not in ['result_renderer', 'on_failure']:
- continue
- pwidget = _get_parameter_widget(
- formlayout.parentWidget(),
- param,
- pname,
- # pass a given value, or indicate that there was none
- cmdkwargs.get(pname, pw._NoValue),
- # will also be _NoValue, if there was none
- param.cmd_kwargs.get('default', pw._NoValue),
- # The generic options are not command specific and have no relation
- # to command specific ones. Hence, allargs shouldn't be needed.
- allargs=None,
- )
- formlayout.addRow(pname, pwidget)
- #
- # Internal helpers
- #
- def _get_params(cmd) -> List:
- """Take a callable and return a list of parameter names, and their defaults
- Parameter names and defaults are returned as 2-tuples. If a parameter has
- no default, the special value `_NoValue` is used.
- """
- # lifted from setup_parser_for_interface()
- args, varargs, varkw, defaults = getargspec(cmd, include_kwonlyargs=True)
- return list(
- zip_longest(
- # fuse parameters from the back, to match with their respective
- # defaults -- if soem have no defaults, they would be the first
- args[::-1],
- defaults[::-1],
- # pad with a dedicate type, to be able to tell if there was a
- # default or not
- fillvalue=pw._NoValue)
- # reverse the order again to match the original order in the signature
- )[::-1]
- def _get_parameter_widget(
- parent: QWidget,
- param: Parameter,
- name: str,
- value: Any = pw._NoValue,
- default: Any = pw._NoValue,
- allargs: Dict or None = None) -> QWidget:
- """Populate a given layout with a data entry widget for a command parameter
- `value` is an explicit setting requested by the caller. A value of
- `_NoValue` indicates that there was no specific value given. `default` is a
- command's default parameter value, with `_NoValue` indicating that the
- command has no default for a parameter.
- """
- # guess the best widget-type based on the argparse setup and configured
- # constraints
- pwid_factory = _get_parameter_widget_factory(
- name, default, param.constraints, param.cmd_kwargs,
- allargs if allargs else dict())
- return pw.load_parameter_widget(
- parent,
- pwid_factory,
- name=name,
- docs=param._doc,
- value=value,
- default=default,
- validator=param.constraints,
- allargs=allargs,
- )
- def _get_parameter_widget_factory(
- name: str,
- default: Any,
- constraints: Callable or None,
- argparse_spec: Dict,
- allargs: Dict) -> Callable:
- """Translate DataLad command parameter specs into Gooey input widgets"""
- # for now just one to play with
- # TODO each factory must provide a standard widget method
- # to return the final value, ready to pass onto the respective
- # parameter of the command call
- argparse_action = argparse_spec.get('action')
- # we must consider the following action specs for widget selection
- # - 'store_const'
- # - 'store_true' and 'store_false'
- # - 'append'
- # - 'append_const'
- # - 'count'
- # - 'extend'
- #if name == 'path':
- # return get_pathselection_widget
- dspath = allargs.get('dataset')
- if hasattr(dspath, 'pathobj'):
- # matches a dataset/repo instance
- dspath = dspath.pathobj
- # if we have no idea, use a simple line edit
- type_widget = pw.StrParamWidget
- if name == 'dataset':
- type_widget = functools.partial(
- pw.PathParamWidget, pathtype=QFileDialog.Directory)
- if name == 'path':
- type_widget = functools.partial(
- pw.PathParamWidget, basedir=dspath)
- if argparse_action in ('store_true', 'store_false'):
- type_widget = pw.BoolParamWidget
- elif isinstance(constraints, EnsureChoice) and argparse_action is None:
- type_widget = functools.partial(
- pw.ChoiceParamWidget, choices=constraints._allowed)
- elif argparse_spec.get('choices'):
- type_widget = functools.partial(
- pw.ChoiceParamWidget, choices=argparse_spec.get('choices'))
- elif name == 'recursion_limit':
- type_widget = functools.partial(pw.PosIntParamWidget, allow_none=True)
- # we must consider the following nargs spec for widget selection
- # (int, '*', '+'), plus action=append
- # in all these cases, we need to expect multiple instances of the data type
- # for which we have selected the input widget above
- argparse_nargs = argparse_spec.get('nargs')
- if (argparse_action == 'append'
- or argparse_nargs in ('+', '*')
- or isinstance(argparse_nargs, int)):
- type_widget = functools.partial(
- # TODO give a fixed N as a parameter too
- MultiValueInputWidget, type_widget)
- return type_widget
|