Skip to content

Commit

Permalink
Make dashes and underscores interchangeable in option names.
Browse files Browse the repository at this point in the history
  • Loading branch information
bdarnell committed Jul 4, 2015
1 parent d7f8c16 commit 463669e
Show file tree
Hide file tree
Showing 3 changed files with 80 additions and 16 deletions.
50 changes: 35 additions & 15 deletions tornado/options.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,12 @@ def connect():
from tornado.options import options, parse_command_line
options.logging = None
parse_command_line()
.. versionchanged:: 4.3
Dashes and underscores are fully interchangeable in option names;
options can be defined, set, and read with any mix of the two.
Dashes are typical for command-line usage while config files require
underscores.
"""

from __future__ import absolute_import, division, print_function, with_statement
Expand Down Expand Up @@ -103,28 +109,38 @@ def __init__(self):
self.define("help", type=bool, help="show this help information",
callback=self._help_callback)

def _normalize_name(self, name):
return name.replace('_', '-')

def __getattr__(self, name):
name = self._normalize_name(name)
if isinstance(self._options.get(name), _Option):
return self._options[name].value()
raise AttributeError("Unrecognized option %r" % name)

def __setattr__(self, name, value):
name = self._normalize_name(name)
if isinstance(self._options.get(name), _Option):
return self._options[name].set(value)
raise AttributeError("Unrecognized option %r" % name)

def __iter__(self):
return iter(self._options)
return (opt.name for opt in self._options.values())

def __contains__(self, name):
name = self._normalize_name(name)
return name in self._options

def __getitem__(self, item):
return self._options[item].value()
def __getitem__(self, name):
name = self._normalize_name(name)
return self._options[name].value()

def items(self):
"""A sequence of (name, value) pairs.
.. versionadded:: 3.1
"""
return [(name, opt.value()) for name, opt in self._options.items()]
return [(opt.name, opt.value()) for name, opt in self._options.items()]

def groups(self):
"""The set of option-groups created by ``define``.
Expand All @@ -151,7 +167,7 @@ def group_dict(self, group):
.. versionadded:: 3.1
"""
return dict(
(name, opt.value()) for name, opt in self._options.items()
(opt.name, opt.value()) for name, opt in self._options.items()
if not group or group == opt.group_name)

def as_dict(self):
Expand All @@ -160,7 +176,7 @@ def as_dict(self):
.. versionadded:: 3.1
"""
return dict(
(name, opt.value()) for name, opt in self._options.items())
(opt.name, opt.value()) for name, opt in self._options.items())

def define(self, name, default=None, type=None, help=None, metavar=None,
multiple=False, group=None, callback=None):
Expand Down Expand Up @@ -223,11 +239,13 @@ def define(self, name, default=None, type=None, help=None, metavar=None,
group_name = group
else:
group_name = file_name
self._options[name] = _Option(name, file_name=file_name,
default=default, type=type, help=help,
metavar=metavar, multiple=multiple,
group_name=group_name,
callback=callback)
normalized = self._normalize_name(name)
option = _Option(name, file_name=file_name,
default=default, type=type, help=help,
metavar=metavar, multiple=multiple,
group_name=group_name,
callback=callback)
self._options[normalized] = option

def parse_command_line(self, args=None, final=True):
"""Parses all options given on the command line (defaults to
Expand Down Expand Up @@ -255,7 +273,7 @@ def parse_command_line(self, args=None, final=True):
break
arg = args[i].lstrip("-")
name, equals, value = arg.partition("=")
name = name.replace('-', '_')
name = self._normalize_name(name)
if name not in self._options:
self.print_help()
raise Error('Unrecognized command line option: %r' % name)
Expand Down Expand Up @@ -287,8 +305,9 @@ def parse_config_file(self, path, final=True):
with open(path, 'rb') as f:
exec_in(native_str(f.read()), config, config)
for name in config:
if name in self._options:
self._options[name].set(config[name])
normalized = self._normalize_name(name)
if normalized in self._options:
self._options[normalized].set(config[name])

if final:
self.run_parse_callbacks()
Expand All @@ -308,7 +327,8 @@ def print_help(self, file=None):
print("\n%s options:\n" % os.path.normpath(filename), file=file)
o.sort(key=lambda option: option.name)
for option in o:
prefix = option.name
# Always print names with dashes in a CLI context.
prefix = self._normalize_name(option.name)
if option.metavar:
prefix += "=" + option.metavar
description = option.help or ""
Expand Down
4 changes: 3 additions & 1 deletion tornado/test/options_test.cfg
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
port=443
port=443
username='李康'
username='李康'

foo_bar='a'
42 changes: 42 additions & 0 deletions tornado/test/options_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -221,3 +221,45 @@ def test_error_redefine(self):
options.define('foo')
self.assertRegexpMatches(str(cm.exception),
'Option.*foo.*already defined')

def test_dash_underscore_cli(self):
# Dashes and underscores should be interchangeable.
for defined_name in ['foo-bar', 'foo_bar']:
for flag in ['--foo-bar=a', '--foo_bar=a']:
options = OptionParser()
options.define(defined_name)
options.parse_command_line(['main.py', flag])
# Attr-style access always uses underscores.
self.assertEqual(options.foo_bar, 'a')
# Dict-style access allows both.
self.assertEqual(options['foo-bar'], 'a')
self.assertEqual(options['foo_bar'], 'a')

def test_dash_underscore_file(self):
# No matter how an option was defined, it can be set with underscores
# in a config file.
for defined_name in ['foo-bar', 'foo_bar']:
options = OptionParser()
options.define(defined_name)
options.parse_config_file(os.path.join(os.path.dirname(__file__),
"options_test.cfg"))
self.assertEqual(options.foo_bar, 'a')

def test_dash_underscore_introspection(self):
# Original names are preserved in introspection APIs.
options = OptionParser()
options.define('with-dash', group='g')
options.define('with_underscore', group='g')
all_options = ['help', 'with-dash', 'with_underscore']
self.assertEqual(sorted(options), all_options)
self.assertEqual(sorted(k for (k, v) in options.items()), all_options)
self.assertEqual(sorted(options.as_dict().keys()), all_options)

self.assertEqual(sorted(options.group_dict('g')),
['with-dash', 'with_underscore'])

# --help shows CLI-style names with dashes.
buf = StringIO()
options.print_help(buf)
self.assertIn('--with-dash', buf.getvalue())
self.assertIn('--with-underscore', buf.getvalue())

0 comments on commit 463669e

Please sign in to comment.