]> git.ipfire.org Git - thirdparty/tornado.git/commitdiff
Make dashes and underscores interchangeable in option names.
authorBen Darnell <ben@bendarnell.com>
Sat, 4 Jul 2015 19:18:26 +0000 (15:18 -0400)
committerBen Darnell <ben@bendarnell.com>
Sat, 4 Jul 2015 19:18:26 +0000 (15:18 -0400)
Closes #421.

tornado/options.py
tornado/test/options_test.cfg
tornado/test/options_test.py

index 89a9e4326539e5d4d330ab1aea2b74d138f97cf3..961bab153b358a56826bc807c17f2a8232a27ad4 100644 (file)
@@ -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 ""
index 09a63060063b0f896a315abdaafc5a04759f036c..cbac8924717124d7a742cbfe5f92b2ed25339e62 100644 (file)
@@ -1,3 +1,5 @@
 port=443
 port=443
-username='李康'
\ No newline at end of file
+username='李康'
+
+foo_bar='a'
index f90c30d251d15ea795947d0701786dcc81d26632..c32184bb405e39b3e02fb4ce56f572a720b19d71 100644 (file)
@@ -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())