Repository: tornadoweb/tornado
Pull request #2412 (2018-06-03 18:26:52)

options: parse_config_file accepts string values
If a config file contains string values for non-string types, these values will be parsed as they would on the command line. This is especially convenient for date/time options.

Replaces #1714 


Patch:
diff --git a/setup.py b/setup.py
index be3e3d1b..6ba89eb4 100644
--- a/setup.py
+++ b/setup.py
@@ -161,6 +161,8 @@ setup(
             "gettext_translations/fr_FR/LC_MESSAGES/tornado_test.mo",
             "gettext_translations/fr_FR/LC_MESSAGES/tornado_test.po",
             "options_test.cfg",
+            "options_test_types.cfg",
+            "options_test_types_str.cfg",
             "static/robots.txt",
             "static/sample.xml",
             "static/sample.xml.gz",
diff --git a/tornado/options.py b/tornado/options.py
index a6f77029..a269d44e 100644
--- a/tornado/options.py
+++ b/tornado/options.py
@@ -365,7 +365,17 @@ class OptionParser(object):
         for name in config:
             normalized = self._normalize_name(name)
             if normalized in self._options:
-                self._options[normalized].set(config[name])
+                option = self._options[normalized]
+                if option.multiple:
+                    if not isinstance(config[name], (list, str)):
+                        raise Error("Option %r is required to be a list of %s "
+                                    "or a comma-separated string" %
+                                    (option.name, option.type.__name__))
+
+                if type(config[name]) == str and option.type != str:
+                    option.parse(config[name])
+                else:
+                    option.set(config[name])
 
         if final:
             self.run_parse_callbacks()


Test patch:
diff --git a/tornado/test/options_test.py b/tornado/test/options_test.py
index 974210b0..9db64be3 100644
--- a/tornado/test/options_test.py
+++ b/tornado/test/options_test.py
@@ -24,6 +24,18 @@ except ImportError:
         mock = None
 
 
+class Email(object):
+    def __init__(self, value):
+        if isinstance(value, str) and '@' in value:
+            self._value = value
+        else:
+            raise ValueError()
+
+    @property
+    def value(self):
+        return self._value
+
+
 class OptionsTest(unittest.TestCase):
     def test_parse_command_line(self):
         options = OptionParser()
@@ -189,7 +201,7 @@ class OptionsTest(unittest.TestCase):
             self.assertEqual(options.foo, 5)
         self.assertEqual(options.foo, 2)
 
-    def test_types(self):
+    def _define_options(self):
         options = OptionParser()
         options.define('str', type=str)
         options.define('basestring', type=basestring_type)
@@ -197,13 +209,11 @@ class OptionsTest(unittest.TestCase):
         options.define('float', type=float)
         options.define('datetime', type=datetime.datetime)
         options.define('timedelta', type=datetime.timedelta)
-        options.parse_command_line(['main.py',
-                                    '--str=asdf',
-                                    '--basestring=qwer',
-                                    '--int=42',
-                                    '--float=1.5',
-                                    '--datetime=2013-04-28 05:16',
-                                    '--timedelta=45s'])
+        options.define('email', type=Email)
+        options.define('list-of-int', type=int, multiple=True)
+        return options
+
+    def _check_options_values(self, options):
         self.assertEqual(options.str, 'asdf')
         self.assertEqual(options.basestring, 'qwer')
         self.assertEqual(options.int, 42)
@@ -211,6 +221,30 @@ class OptionsTest(unittest.TestCase):
         self.assertEqual(options.datetime,
                          datetime.datetime(2013, 4, 28, 5, 16))
         self.assertEqual(options.timedelta, datetime.timedelta(seconds=45))
+        self.assertEqual(options.email.value, 'tornado@web.com')
+        self.assertTrue(isinstance(options.email, Email))
+        self.assertEqual(options.list_of_int, [1, 2, 3])
+
+    def test_types(self):
+        options = self._define_options()
+        options.parse_command_line(['main.py',
+                                    '--str=asdf',
+                                    '--basestring=qwer',
+                                    '--int=42',
+                                    '--float=1.5',
+                                    '--datetime=2013-04-28 05:16',
+                                    '--timedelta=45s',
+                                    '--email=tornado@web.com',
+                                    '--list-of-int=1,2,3'])
+        self._check_options_values(options)
+
+    def test_types_with_conf_file(self):
+        for config_file_name in ("options_test_types.cfg",
+                                 "options_test_types_str.cfg"):
+            options = self._define_options()
+            options.parse_config_file(os.path.join(os.path.dirname(__file__),
+                                      config_file_name))
+            self._check_options_values(options)
 
     def test_multiple_string(self):
         options = OptionParser()
diff --git a/tornado/test/options_test_types.cfg b/tornado/test/options_test_types.cfg
new file mode 100644
index 00000000..e1d53cb9
--- /dev/null
+++ b/tornado/test/options_test_types.cfg
@@ -0,0 +1,11 @@
+from datetime import datetime, timedelta
+from tornado.test.options_test import Email
+
+str = 'asdf'
+basestring = 'qwer'
+int = 42
+float = 1.5
+datetime = datetime(2013, 4, 28, 5, 16)
+timedelta = timedelta(0, 45)
+email = Email('tornado@web.com')
+list_of_int = [1, 2, 3]
diff --git a/tornado/test/options_test_types_str.cfg b/tornado/test/options_test_types_str.cfg
new file mode 100644
index 00000000..25dfbc2b
--- /dev/null
+++ b/tornado/test/options_test_types_str.cfg
@@ -0,0 +1,8 @@
+str = 'asdf'
+basestring = 'qwer'
+int = 42
+float = 1.5
+datetime = '2013-04-28 05:16'
+timedelta = '45s'
+email = 'tornado@web.com'
+list_of_int = '1,2,3'