Skip to content

Commit

Permalink
Assume positional arguments are required
Browse files Browse the repository at this point in the history
The 'positional' keyword specifically applies to oslo.config's argparse
support. Unlike oslo.config, argparse assumes that all positional
arguments are required by default, and you have to explicitly tell it
that a positional argument is optional if you'd like to opt into that
behavior.

This patch adopts that same behavior for oslo.config. When you define an
option to be non-positional (oslo.config's default, designed for config
files), then oslo.config makes that option optional:

However, when you define an option to be positional, oslo.config assumes
that the option is primarily going to be used on the CLI and thus sets
it as required, by default.

This change in behavior has the side effect of allowing argparse to
enforce required arguments on the CLI *while* parsing arguments, instead
of depending on oslo.config to detect the condition *after* argparse has
been allowed to parse "invalid" arguments. argparse correctly raises a
SystemExit in this case, and prints the actual command usage and a "hey,
you forgot this required argument", instead of allowing oslo.config to
dump a backtrace to the CLI with a context-less error message
("context-less" in that no useful CLI usage information is dumped along
with the crash to help you correct the condition).

Change-Id: Ifdc6918444fe72f7e1649483c237cce64b4c72d8
Partial-Bug: 1676989
  • Loading branch information
dolph authored and stephenfin committed Mar 30, 2017
1 parent ec84eed commit 18d1617
Show file tree
Hide file tree
Showing 4 changed files with 34 additions and 27 deletions.
4 changes: 4 additions & 0 deletions doc/source/reference/command-line.rst
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@ constructor argument:
>>> conf.bar
['a', 'b']
By default, positional arguments are also required. You may opt-out of this
behavior by setting ``required=False``, to have an optional positional
argument.

Sub-Parsers
-----------

Expand Down
9 changes: 7 additions & 2 deletions oslo_config/cfg.py
Original file line number Diff line number Diff line change
Expand Up @@ -540,7 +540,7 @@ class Opt(object):

def __init__(self, name, type=None, dest=None, short=None,
default=None, positional=False, metavar=None, help=None,
secret=False, required=False,
secret=False, required=None,
deprecated_name=None, deprecated_group=None,
deprecated_opts=None, sample_default=None,
deprecated_for_removal=False, deprecated_reason=None,
Expand All @@ -556,6 +556,11 @@ def __init__(self, name, type=None, dest=None, short=None,
raise TypeError('type must be callable')
self.type = type

# By default, non-positional options are *optional*, and positional
# options are *required*.
if required is None:
required = True if positional else False

if dest is None:
self.dest = self.name.replace('-', '_')
else:
Expand Down Expand Up @@ -751,7 +756,7 @@ def _get_argparse_kwargs(self, group, **kwargs):
if group is not None:
dest = group.name + '_' + dest
kwargs['dest'] = dest
else:
elif not self.required:
kwargs['nargs'] = '?'
kwargs.update({'default': None,
'metavar': self.metavar,
Expand Down
36 changes: 11 additions & 25 deletions oslo_config/tests/test_cfg.py
Original file line number Diff line number Diff line change
Expand Up @@ -234,7 +234,8 @@ def test_print_sorted_help(self):

def test_print_sorted_help_with_positionals(self):
f = moves.StringIO()
self.conf.register_cli_opt(cfg.StrOpt('pst', positional=True))
self.conf.register_cli_opt(
cfg.StrOpt('pst', positional=True, required=False))
self.conf.register_cli_opt(cfg.StrOpt('abc'))
self.conf.register_cli_opt(cfg.StrOpt('zba'))
self.conf.register_cli_opt(cfg.StrOpt('ghi'))
Expand Down Expand Up @@ -813,7 +814,8 @@ class PositionalTestCase(BaseTestCase):
def _do_pos_test(self, opt_class, default, cli_args, value):
self.conf.register_cli_opt(opt_class('foo',
default=default,
positional=True))
positional=True,
required=False))

self.conf(cli_args)

Expand Down Expand Up @@ -940,19 +942,15 @@ def test_required_positional_opt_undefined(self):
self.assertRaises(SystemExit, self.conf, ['--help'])
self.assertIn(' foo\n', sys.stdout.getvalue())

self.assertRaises(cfg.RequiredOptError, self.conf, [])
self.assertRaises(SystemExit, self.conf, [])

def test_optional_positional_opt_defined(self):
self.conf.register_cli_opt(
cfg.StrOpt('foo', required=False, positional=True))

self.useFixture(fixtures.MonkeyPatch('sys.stdout', moves.StringIO()))
self.assertRaises(SystemExit, self.conf, ['--help'])
# FIXME(dolphm): Due to bug 1676989, this argument appears as a
# required argument in the CLI help. Instead, the following
# commented-out code should work:
# self.assertIn(' [foo]\n', sys.stdout.getvalue())
self.assertIn(' foo\n', sys.stdout.getvalue())
self.assertIn(' [foo]\n', sys.stdout.getvalue())

self.conf(['bar'])

Expand All @@ -965,11 +963,7 @@ def test_optional_positional_opt_undefined(self):

self.useFixture(fixtures.MonkeyPatch('sys.stdout', moves.StringIO()))
self.assertRaises(SystemExit, self.conf, ['--help'])
# FIXME(dolphm): Due to bug 1676989, this argument appears as a
# required argument in the CLI help. Instead, the following
# commented-out code should work:
# self.assertIn(' [foo]\n', sys.stdout.getvalue())
self.assertIn(' foo\n', sys.stdout.getvalue())
self.assertIn(' [foo]\n', sys.stdout.getvalue())

self.conf([])

Expand All @@ -982,11 +976,7 @@ def test_optional_positional_hyphenated_opt_defined(self):

self.useFixture(fixtures.MonkeyPatch('sys.stdout', moves.StringIO()))
self.assertRaises(SystemExit, self.conf, ['--help'])
# FIXME(dolphm): Due to bug 1676989, this argument appears as a
# required argument in the CLI help. Instead, the following
# commented-out code should work:
# self.assertIn(' [foo-bar]\n', sys.stdout.getvalue())
self.assertIn(' foo-bar\n', sys.stdout.getvalue())
self.assertIn(' [foo-bar]\n', sys.stdout.getvalue())

self.conf(['baz'])
self.assertTrue(hasattr(self.conf, 'foo_bar'))
Expand All @@ -1002,11 +992,7 @@ def test_optional_positional_hyphenated_opt_undefined(self):

self.useFixture(fixtures.MonkeyPatch('sys.stdout', moves.StringIO()))
self.assertRaises(SystemExit, self.conf, ['--help'])
# FIXME(dolphm): Due to bug 1676989, this argument appears as a
# required argument in the CLI help. Instead, the following
# commented-out code should work:
# self.assertIn(' [foo-bar]\n', sys.stdout.getvalue())
self.assertIn(' foo-bar\n', sys.stdout.getvalue())
self.assertIn(' [foo-bar]\n', sys.stdout.getvalue())

self.conf([])
self.assertTrue(hasattr(self.conf, 'foo_bar'))
Expand Down Expand Up @@ -1036,12 +1022,12 @@ def test_required_positional_hyphenated_opt_undefined(self):
self.assertRaises(SystemExit, self.conf, ['--help'])
self.assertIn(' foo-bar\n', sys.stdout.getvalue())

self.assertRaises(cfg.RequiredOptError, self.conf, [])
self.assertRaises(SystemExit, self.conf, [])

def test_missing_required_cli_opt(self):
self.conf.register_cli_opt(
cfg.StrOpt('foo', required=True, positional=True))
self.assertRaises(cfg.RequiredOptError, self.conf, [])
self.assertRaises(SystemExit, self.conf, [])

def test_positional_opts_order(self):
self.conf.register_cli_opts((
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
---
upgrade:
- |
Positional options are now required by default, to match argparse's default
behavior. To revert this behavior (and maintain optional positional
arguments), you need to explicitly specify ``positional=True,
required=False`` as part of the options definition.
fixes:
- |
On the command line, oslo.config now returns command usage information from
argparse (instead of dumping a backtrace) when required arguments are
missing.

0 comments on commit 18d1617

Please sign in to comment.