From 463669e9e085d6048e233db5a47a9e8895ff554b Mon Sep 17 00:00:00 2001 From: Ben Darnell Date: Sat, 4 Jul 2015 15:18:26 -0400 Subject: [PATCH] Make dashes and underscores interchangeable in option names. Closes #421. --- tornado/options.py | 50 ++++++++++++++++++++++++----------- tornado/test/options_test.cfg | 4 ++- tornado/test/options_test.py | 42 +++++++++++++++++++++++++++++ 3 files changed, 80 insertions(+), 16 deletions(-) diff --git a/tornado/options.py b/tornado/options.py index 89a9e4326..961bab153 100644 --- a/tornado/options.py +++ b/tornado/options.py @@ -68,6 +68,12 @@ instances to define isolated sets of options, such as for subcommands. 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 @@ -103,28 +109,38 @@ class OptionParser(object): 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``. @@ -151,7 +167,7 @@ class OptionParser(object): .. 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): @@ -160,7 +176,7 @@ class OptionParser(object): .. 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): @@ -223,11 +239,13 @@ class OptionParser(object): 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 @@ -255,7 +273,7 @@ class OptionParser(object): 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) @@ -287,8 +305,9 @@ class OptionParser(object): 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() @@ -308,7 +327,8 @@ class OptionParser(object): 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 "" diff --git a/tornado/test/options_test.cfg b/tornado/test/options_test.cfg index 09a630600..cbac89247 100644 --- a/tornado/test/options_test.cfg +++ b/tornado/test/options_test.cfg @@ -1,3 +1,5 @@ port=443 port=443 -username='李康' \ No newline at end of file +username='李康' + +foo_bar='a' diff --git a/tornado/test/options_test.py b/tornado/test/options_test.py index f90c30d25..c32184bb4 100644 --- a/tornado/test/options_test.py +++ b/tornado/test/options_test.py @@ -221,3 +221,45 @@ class OptionsTest(unittest.TestCase): 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()) -- 2.47.2