Skip to content

Commit

Permalink
Allow CylcOptionParser to display docs for a command which
Browse files Browse the repository at this point in the history
might have multiple different valid arguments.

e.g. Cylc message can be either
cylc message -- [WORKFLOW JOB [[SEVERITY:]MESSAGE ...]]]
__or__
cylc message --	[[SEVERITY:]MESSAGE ...]]]

- Refactored part of the __init__ method of CylcOptionParser.
  into a method for ease of testing.
- Added unit tests to get complete coverage of new method.

Response to review: the ... from the second usage.
Added extra information to the descriptions of the args.

remove cli-help test
  • Loading branch information
wxtim committed Oct 4, 2024
1 parent 372e01e commit 1fd2de3
Show file tree
Hide file tree
Showing 3 changed files with 115 additions and 23 deletions.
56 changes: 40 additions & 16 deletions cylc/flow/option_parsers.py
Original file line number Diff line number Diff line change
Expand Up @@ -369,6 +369,8 @@ class CylcOptionParser(OptionParser):
)
]

LINEBREAK = ('\n', '')

def __init__(
self,
usage: str,
Expand Down Expand Up @@ -407,7 +409,6 @@ def __init__(
if multitask:
usage += self.MULTITASK_USAGE

args = ""
self.n_compulsory_args = 0
self.n_optional_args = 0
self.unlimited_args = False
Expand All @@ -419,21 +420,7 @@ def __init__(
self.segregated_log = segregated_log

if argdoc:
maxlen = max(len(arg) for arg, _ in argdoc)
usage += "\n\nArguments:"
for arg, descr in argdoc:
if arg.startswith('['):
self.n_optional_args += 1
else:
self.n_compulsory_args += 1
if arg.rstrip(']').endswith('...'):
self.unlimited_args = True

args += arg + " "

pad = (maxlen - len(arg)) * ' ' + ' '
usage += "\n " + arg + pad + descr
usage = usage.replace('ARGS', args)
usage = self.argdoc_parser(argdoc, usage)

OptionParser.__init__(
self,
Expand All @@ -442,6 +429,43 @@ def __init__(
formatter=CylcHelpFormatter()
)

def argdoc_parser(self, argdoc, usage) -> str:
"""Convert argdoc list into a string to be passed to
OptionParser.__init__ as usage argument.
"""
args = ""
maxlen = max(len(arg) for arg, _ in argdoc)
usage += "\n\nArguments:"
if 'ARGS' in usage:
script_name, usage = usage.split('ARGS', 1)
else:
return usage
used_args = [self.LINEBREAK]
if self.LINEBREAK in argdoc:
args += ' One of:\n'
args += script_name
for arg, descr in argdoc:
if arg.startswith('['):
self.n_optional_args += 1
else:
self.n_compulsory_args += 1
if arg.rstrip(']').endswith('...'):
self.unlimited_args = True

if arg == '\n':
args += arg
args += script_name
else:
args += arg + " "

if (arg, descr) not in used_args:
pad = (maxlen - len(arg)) * ' ' + ' '
usage += "\n " + arg + pad + descr
used_args.append((arg, descr))

usage = args + usage
return usage

def get_std_options(self):
"""Get a data-structure of standard options"""
opts = []
Expand Down
11 changes: 6 additions & 5 deletions cylc/flow/scripts/message.py
Original file line number Diff line number Diff line change
Expand Up @@ -117,13 +117,14 @@ def get_option_parser() -> COP:
__doc__,
comms=True,
argdoc=[
COP.optional(WORKFLOW_ID_ARG_DOC),
WORKFLOW_ID_ARG_DOC,
('JOB', 'Job ID - CYCLE/TASK_NAME/SUBMIT_NUM'),
COP.optional(
('JOB', 'Job ID - CYCLE/TASK_NAME/SUBMIT_NUM')
('[SEVERITY:]MESSAGE ...', 'Severity Level:Message')
),
COP.optional(
('[SEVERITY:]MESSAGE ...', 'Messages')
)
COP.LINEBREAK,
COP.optional(('[SEVERITY:]MESSAGE', 'Severity Level:Message')),
COP.optional(('[SEVERITY:]MESSAGE', 'Severity Level:Message')),
]
)

Expand Down
71 changes: 69 additions & 2 deletions tests/unit/test_option_parsers.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,13 +30,20 @@
)


USAGE_WITH_COMMENT = "usage \n # comment"
USAGE_WITH_COMMENT = "ARGS usage \n # comment"
ARGS = 'args'
KWARGS = 'kwargs'
SOURCES = 'sources'
USEIF = 'useif'


class MockCylcOptionParser(COP):
"""A stub subclass to allow unit-like testing of methods."""
def __init__(self, **kwargs):
for key, value in kwargs.items():
self.__dict__[key] = value


@pytest.fixture(scope='module')
def parser():
return COP(
Expand Down Expand Up @@ -94,7 +101,9 @@ def test_help_nocolor(monkeypatch: pytest.MonkeyPatch, parser: COP):
f = io.StringIO()
with redirect_stdout(f):
parser.print_help()
assert (f.getvalue()).startswith("Usage: " + USAGE_WITH_COMMENT)
str_ = f.getvalue()
assert str_.startswith(
"Usage: " + USAGE_WITH_COMMENT.replace('ARGS', 'SOME_ARG '))


def test_Options_std_opts():
Expand Down Expand Up @@ -580,3 +589,61 @@ def test__in_list():
second = OptionSettings(['--foo'])
third = OptionSettings(['--bar'])
assert first._in_list([second, third]) is True

@pytest.mark.parametrize(
'argdoc, usage, expect',
(
param(
[('WORKFLOW', 'Workflow ID')],
'cylc play [OPTIONS] ARGS\n\nStart, resume...',
enumerate(('cylc play [OPTIONS] WORKFLOW',)),
id='basic'
),
param(
[
('WORKFLOW', 'Workflow ID'),
COP.LINEBREAK,
('[TASK]', 'Task ID'),
],
'cylc total-perspective [OPTIONS] ARGS\n\nThe most savage...',
enumerate([
'One of:',
'cylc total-perspective [OPTIONS] WORKFLOW',
'cylc total-perspective [OPTIONS] [TASK]'
]),
id='has-line-break'
),
param(
[('[TASK]', 'Task ID')],
'cylc infinite-improbability [OPTIONS] ARGS\n\nEvery point...',
enumerate(['cylc infinite-improbability [OPTIONS] [TASK]',]),
id='startswith-sq-bracket',
),
param(
[
('WORKFLOW', 'Workflow ID'),
COP.LINEBREAK,
('WORKFLOW', 'Workflow ID'),
],
'cylc skin-cat [OPTIONS] ARGS\n\nThere are many ways.',
{
-2: 'Arguments:',
-1: 'WORKFLOW Workflow ID'
}.items(),
id='no-repeated-arg-descriptions'
)
)
)
def test_argdoc_parser(self, argdoc, usage, expect):
"""Tests for standalone argdoc_parser method when argdoc
offers multiple ways to call a command.
"""
parser = MockCylcOptionParser(
n_compulsory_args=0,
n_optional_args=0,
unlimited_args=False)

result = parser.argdoc_parser(argdoc, usage)
results = [r for r in result.split('\n') if r]
for i, line in expect:
assert results[i].strip() == line

0 comments on commit 1fd2de3

Please sign in to comment.