Commit 5851e4c0 authored by Paul McCarthy's avatar Paul McCarthy 🚵
Browse files

Merge branch 'bf/wrapper-singlechar-args' into 'master'

RF: New options to applyArgStyle

See merge request fsl/fslpy!335
parents 281160db 169e2a66
Pipeline #14141 canceled with stages
in 4 seconds
......@@ -3,6 +3,21 @@ order.
3.9.1 (Friday 13th May 2022)
----------------------------
Changed
^^^^^^^
* Adjusted the :func:`.applyArgStyle` function so that it allows separate
specification of the style to use for single-character arguments. This
fixes some usage issues with commands such as FSL ``fast``, which have
regular ``--=`` arguments, but also single-character arguments which
expect multiple positional values (!335).
3.9.0 (Tuesday 12th April 2022)
-------------------------------
......
......@@ -252,7 +252,8 @@ def _dryrun(submit, returnStdout, returnStderr, returnExitcode, *args):
results = []
stderr = ''
stdout = ' '.join(args)
join = getattr(shlex, 'join', ' '.join)
stdout = join(args)
if returnStdout: results.append(stdout)
if returnStderr: results.append(stderr)
......
......@@ -51,10 +51,8 @@ def fast(imgs, out='fast', **kwargs):
}
cmd = ['fast', '--out=%s' % out]
cmd += wutils.applyArgStyle('--=',
valmap=valmap,
cmd += wutils.applyArgStyle(valmap=valmap,
argmap=argmap,
singlechar_args=True,
**kwargs)
cmd += imgs
......
......@@ -243,18 +243,24 @@ generated command line arguments.
"""
def applyArgStyle(style,
def applyArgStyle(style=None,
valsep=None,
argmap=None,
valmap=None,
singlechar_args=False,
charstyle=None,
charsep=None,
**kwargs):
"""Turns the given ``kwargs`` into command line options. This function
is intended to be used to automatically generate command line options
from arguments passed into a Python function.
The ``style`` and ``valsep`` arguments control how key-value pairs
are converted into command-line options:
The default settings will generate arguments that match typical UNIX
conventions, e.g. ``-a val``, ``--arg=val``, ``-a val1 val2``,
``--arg=val1,val2``.
The ``style`` and ``valsep`` (and ``charstyle`` and ``charsep``) arguments
control how key-value pairs are converted into command-line options:
========= ========== ===========================
......@@ -275,86 +281,117 @@ def applyArgStyle(style,
========= ========== ===========================
:arg style: Controls how the ``kwargs`` are converted into command-line
options - must be one of ``'-'``, ``'--'``, ``'-='``, or
``'--='``.
:arg style: Controls how the ``kwargs`` are converted into command-line
options - must be one of ``'-'``, ``'--'``, ``'-='``, or
``'--='`` (the default).
:arg valsep: Controls how the values passed to command-line options
which expect multiple arguments are delimited - must be
one of ``' '``, ``','`` or ``'"'``. Defaults to ``' '``
if ``'=' not in style``, ``','`` otherwise.
:arg argmap: Dictionary of ``{kwarg-name : cli-name}`` mappings. This be
used if you want to use different argument names in your
Python function for the command-line options.
:arg valsep: Controls how the values passed to command-line options
which expect multiple arguments are delimited - must be
one of ``' '``, ``','`` or ``'"'``. Defaults to ``' '``
if ``'=' not in style``, ``','`` otherwise.
:arg valmap: Dictionary of ``{cli-name : value}`` mappings. This can be
used to define specific semantics for some command-line
options. Acceptable values for ``value`` are as follows
:arg argmap: Dictionary of ``{kwarg-name : cli-name}`` mappings. This can
be used if you want to use different argument names in your
Python function for the command-line options.
- :data:`SHOW_IF_TRUE` - if the argument is present, and
``True`` in ``kwargs``, the command line option
will be added (without any arguments).
:arg valmap: Dictionary of ``{cli-name : value}`` mappings. This can be
used to define specific semantics for some command-line
options. Acceptable values for ``value`` are as follows
- :data:`HIDE_IF_TRUE` - if the argument is present, and
``False`` in ``kwargs``, the command line option
will be added (without any arguments).
- :data:`SHOW_IF_TRUE` - if the argument is present, and
``True`` in ``kwargs``, the command line option
will be added (without any arguments).
- Any other constant value. If the argument is present
in ``kwargs``, its command-line option will be
added, with the constant value as its argument.
- :data:`HIDE_IF_TRUE` - if the argument is present, and
``False`` in ``kwargs``, the command line option
will be added (without any arguments).
The argument for any options not specified in the
``valmap`` will be converted into strings.
- Any other constant value. If the argument is present
in ``kwargs``, its command-line option will be
added, with the constant value as its argument.
:arg charstyle: Separate style specification for single-character
arguments. If ``style == '--='``, defaults to ``'-'``,
matching UNIX conventions. Otherwise defaults to the
value of ``style``.
The argument for any options not specified in the ``valmap``
will be converted into strings.
:arg charsep: Controls how the values passed to command-line options
which expect multiple arguments are delimited - must be
one of ``' '``, ``','`` or ``'"'``. Defaults to ``' '``
if ``'=' not in style``, ``','`` otherwise.
:arg singlechar_args: If True, single character arguments always take a
single hyphen prefix (e.g. -h) regardless of the
style.
:arg singlechar_args: If ``True``, equivalent to ``charstyle='-'``.
:arg kwargs: Arguments to be converted into command-line options.
:returns: A list containing the generated command-line options.
"""
if style is None:
style = '--='
if charstyle is None:
if singlechar_args: charstyle = '-'
elif style == '--=': charstyle = '-'
else: charstyle = style
if valsep is None:
if '=' in style: valsep = ','
else: valsep = ' '
if charsep is None:
if '=' in charstyle: charsep = ','
else: charsep = ' '
if style not in ('-', '--', '-=', '--='):
raise ValueError('Invalid style: {}'.format(style))
raise ValueError(f'Invalid style: {style}')
if charstyle not in ('-', '--', '-=', '--='):
raise ValueError(f'Invalid charstyle: {charstyle}')
if valsep not in (' ', ',', '"'):
raise ValueError('Invalid valsep: {}'.format(valsep))
raise ValueError(f'Invalid valsep: {valsep}')
if charsep not in (' ', ',', '"'):
raise ValueError(f'Invalid charsep: {charsep}')
# we don't handle the case where '=' in
# style, and valsep == ' ', because no
# sane CLI app would do this. Right?
# It makes no sense to combine argument+value
# with an equals sign, but not have the value
# quoted (e.g "--arg=val1 val2 val3").
if '=' in style and valsep == ' ':
raise ValueError('Incompatible style and valsep: s={} v={}'.format(
style, valsep))
raise ValueError(f'Incompatible style {style} '
'and valsep ({valsep})')
if '=' in charstyle and charsep == ' ':
raise ValueError(f'Incompatible style {charstyle} '
'and valsep ({charsep})')
if argmap is None: argmap = {}
if valmap is None: valmap = {}
def fmtarg(arg):
if style in ('-', '-=') or (singlechar_args and len(arg) == 1):
arg = '-{}'.format(arg)
elif style in ('--', '--='):
arg = '--{}'.format(arg)
return arg
# always returns a sequence
def fmtval(val):
# Format the argument.
def fmtarg(arg, style):
if style in ('--', '--='): return f'--{arg}'
else: return f'-{arg}'
# Formt the argument value. Always returns
# a sequence. We don't add quotes around
# values - instead we just ensure that
# arguments+values are grouped correctly in
# the final result (the same as what
# shlex.split would generate for a properly
# quoted string).
def fmtval(val, sep):
if isinstance(val, abc.Sequence) and (not isinstance(val, str)):
val = [str(v) for v in val]
if valsep == ' ': return val
elif valsep == '"': return [' ' .join(val)]
else: return [valsep.join(val)]
if sep == ' ': return val
elif sep == '"': return [' '.join(val)]
else: return [sep.join(val)]
else:
return [str(val)]
# val is assumed to be a sequence
def fmtargval(arg, val):
# Format the argument and value together.
# val is assumed to be a sequence.
def fmtargval(arg, val, style):
# if '=' in style, val will
# always be a single string
if '=' in style: return ['{}={}'.format(arg, val[0])]
......@@ -366,16 +403,19 @@ def applyArgStyle(style,
if v is None: continue
if len(k) == 1: sty, sep = charstyle, charsep
else: sty, sep = style, valsep
k = argmap.get(k, k)
mapv = valmap.get(k, fmtval(v))
k = fmtarg(k)
mapv = valmap.get(k, fmtval(v, sep))
k = fmtarg(k, sty)
if mapv in (SHOW_IF_TRUE, HIDE_IF_TRUE):
if (mapv is SHOW_IF_TRUE and v) or \
(mapv is HIDE_IF_TRUE and not v):
args.append(k)
else:
args.extend(fmtargval(k, mapv))
args.extend(fmtargval(k, mapv, sty))
return args
......
......@@ -27,6 +27,10 @@ def checkResult(cmd, base, args, stripdir=None):
Pre python 3.7, we couldn't control the order in which command
line args were generated, so we needed to test all possible orderings.
But for Python >= 3.7, the order in which kwargs are passed will
be the same as the order in which they are rendered, so this function
is not required.
:arg cmd: Generated command
:arg base: Beginning of expected command
:arg args: Sequence of expected arguments
......@@ -366,6 +370,12 @@ def test_fast():
expected = [cmd, '--out=myseg', '--class=3', '--verbose', 'in1', 'in2', 'in3']
assert result.stdout[0] == ' '.join(expected)
result = fw.fast(('in1', 'in2', 'in3'), 'myseg', n_classes=3,
a='reg.mat', A=('csf', 'gm', 'wm'), Prior=True)
expected = [cmd, '--out=myseg', '--class=3', '-a', 'reg.mat',
'-A', 'csf', 'gm', 'wm', '--Prior', 'in1', 'in2', 'in3']
assert result.stdout[0] == ' '.join(expected)
def test_fsl_anat():
with asrt.disabled(), \
......
......@@ -30,11 +30,24 @@ from .. import mockFSLDIR, cleardir, checkdir, testdir, touch
from ..test_run import mock_fsl_sub
def test_applyArgStyle_default():
kwargs = {
'arg1' : 'val',
'arg2' : ['val1', 'val2'],
'a' : 'val',
'b' : ['val1', 'val2'],
}
exp = ['--arg1=val', '--arg2=val1,val2', '-a', 'val', '-b', 'val1', 'val2']
assert wutils.applyArgStyle(**kwargs) == exp
def test_applyArgStyle():
kwargs = {
'name' : 'val',
'name2' : ['val1', 'val2'],
'name3' : 'val1 val2',
}
# these combinations of style+valsep should
......@@ -51,28 +64,70 @@ def test_applyArgStyle():
wutils.applyArgStyle('-', valsep='b', **kwargs)
# style, valsep, expected_result.
# Order of arguments is not guaranteed
tests = [
('-', ' ', [' -name val', '-name2 val1 val2']),
('-', '"', [' -name val', '-name2 "val1 val2"']),
('-', ',', [' -name val', '-name2 val1,val2']),
('-', ' ', ['-name', 'val', '-name2', 'val1', 'val2', '-name3', 'val1 val2']),
('-', '"', ['-name', 'val', '-name2', 'val1 val2', '-name3', 'val1 val2']),
('-', ',', ['-name', 'val', '-name2', 'val1,val2', '-name3', 'val1 val2']),
('--', ' ', ['--name val', '--name2 val1 val2']),
('--', '"', ['--name val', '--name2 "val1 val2"']),
('--', ',', ['--name val', '--name2 val1,val2']),
('--', ' ', ['--name', 'val', '--name2', 'val1', 'val2', '--name3', 'val1 val2']),
('--', '"', ['--name', 'val', '--name2', 'val1 val2', '--name3', 'val1 val2']),
('--', ',', ['--name', 'val', '--name2', 'val1,val2', '--name3', 'val1 val2']),
('-=', '"', [' -name=val', '-name2="val1 val2"']),
('-=', ',', [' -name=val', '-name2=val1,val2']),
('-=', '"', ['-name=val', '-name2=val1 val2', '-name3=val1 val2']),
('-=', ',', ['-name=val', '-name2=val1,val2', '-name3=val1 val2']),
('--=', '"', ['--name=val', '--name2="val1 val2"']),
('--=', ',', ['--name=val', '--name2=val1,val2']),
('--=', '"', ['--name=val', '--name2=val1 val2', '--name3=val1 val2']),
('--=', ',', ['--name=val', '--name2=val1,val2', '--name3=val1 val2']),
]
for style, valsep, exp in tests:
exp = [shlex.split(e) for e in exp]
result = wutils.applyArgStyle(style, valsep=valsep, **kwargs)
assert result == exp
def test_applyArgStyle_charstyle():
kwargs = {
'n' : 'val',
'm' : ['val1', 'val2'],
'o' : 'val1 val2',
}
# these combinations of charstyle+
# charsep should raise an error
with pytest.raises(ValueError):
wutils.applyArgStyle(charstyle='--=', charsep=' ', **kwargs)
with pytest.raises(ValueError):
wutils.applyArgStyle(charstyle='-=', charsep=' ', **kwargs)
# unsupported chrastyle/charsep
with pytest.raises(ValueError):
wutils.applyArgStyle(charstyle='?', **kwargs)
with pytest.raises(ValueError):
wutils.applyArgStyle('-', charsep='b', **kwargs)
# style, valsep, charstyle, charsep, expected_result.
# Order of arguments is not guaranteed
tests = [
('-', ' ', ['-n', 'val', '-m', 'val1', 'val2', '-o', 'val1 val2']),
('-', '"', ['-n', 'val', '-m', 'val1 val2', '-o', 'val1 val2']),
('-', ',', ['-n', 'val', '-m', 'val1,val2', '-o', 'val1 val2']),
('--', ' ', ['--n', 'val', '--m', 'val1','val2', '--o', 'val1 val2']),
('--', '"', ['--n', 'val', '--m', 'val1 val2', '--o', 'val1 val2']),
('--', ',', ['--n', 'val', '--m', 'val1,val2', '--o', 'val1 val2']),
('-=', '"', ['-n=val', '-m=val1 val2', '-o=val1 val2']),
('-=', ',', ['-n=val', '-m=val1,val2', '-o=val1 val2']),
('--=', '"', ['--n=val', '--m=val1 val2', '--o=val1 val2']),
('--=', ',', ['--n=val', '--m=val1,val2', '--o=val1 val2']),
]
for style, valsep, exp in tests:
result = wutils.applyArgStyle(charstyle=style, charsep=valsep, **kwargs)
assert result in (exp[0] + exp[1], exp[1] + exp[0])
assert result == exp
def test_applyArgStyle_argmap():
......
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment