options_test.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329
  1. # -*- coding: utf-8 -*-
  2. import datetime
  3. from io import StringIO
  4. import os
  5. import sys
  6. from unittest import mock
  7. import unittest
  8. from tornado.options import OptionParser, Error
  9. from tornado.util import basestring_type
  10. from tornado.test.util import subTest
  11. import typing
  12. if typing.TYPE_CHECKING:
  13. from typing import List # noqa: F401
  14. class Email(object):
  15. def __init__(self, value):
  16. if isinstance(value, str) and "@" in value:
  17. self._value = value
  18. else:
  19. raise ValueError()
  20. @property
  21. def value(self):
  22. return self._value
  23. class OptionsTest(unittest.TestCase):
  24. def test_parse_command_line(self):
  25. options = OptionParser()
  26. options.define("port", default=80)
  27. options.parse_command_line(["main.py", "--port=443"])
  28. self.assertEqual(options.port, 443)
  29. def test_parse_config_file(self):
  30. options = OptionParser()
  31. options.define("port", default=80)
  32. options.define("username", default="foo")
  33. options.define("my_path")
  34. config_path = os.path.join(
  35. os.path.dirname(os.path.abspath(__file__)), "options_test.cfg"
  36. )
  37. options.parse_config_file(config_path)
  38. self.assertEqual(options.port, 443)
  39. self.assertEqual(options.username, "李康")
  40. self.assertEqual(options.my_path, config_path)
  41. def test_parse_callbacks(self):
  42. options = OptionParser()
  43. self.called = False
  44. def callback():
  45. self.called = True
  46. options.add_parse_callback(callback)
  47. # non-final parse doesn't run callbacks
  48. options.parse_command_line(["main.py"], final=False)
  49. self.assertFalse(self.called)
  50. # final parse does
  51. options.parse_command_line(["main.py"])
  52. self.assertTrue(self.called)
  53. # callbacks can be run more than once on the same options
  54. # object if there are multiple final parses
  55. self.called = False
  56. options.parse_command_line(["main.py"])
  57. self.assertTrue(self.called)
  58. def test_help(self):
  59. options = OptionParser()
  60. try:
  61. orig_stderr = sys.stderr
  62. sys.stderr = StringIO()
  63. with self.assertRaises(SystemExit):
  64. options.parse_command_line(["main.py", "--help"])
  65. usage = sys.stderr.getvalue()
  66. finally:
  67. sys.stderr = orig_stderr
  68. self.assertIn("Usage:", usage)
  69. def test_subcommand(self):
  70. base_options = OptionParser()
  71. base_options.define("verbose", default=False)
  72. sub_options = OptionParser()
  73. sub_options.define("foo", type=str)
  74. rest = base_options.parse_command_line(
  75. ["main.py", "--verbose", "subcommand", "--foo=bar"]
  76. )
  77. self.assertEqual(rest, ["subcommand", "--foo=bar"])
  78. self.assertTrue(base_options.verbose)
  79. rest2 = sub_options.parse_command_line(rest)
  80. self.assertEqual(rest2, [])
  81. self.assertEqual(sub_options.foo, "bar")
  82. # the two option sets are distinct
  83. try:
  84. orig_stderr = sys.stderr
  85. sys.stderr = StringIO()
  86. with self.assertRaises(Error):
  87. sub_options.parse_command_line(["subcommand", "--verbose"])
  88. finally:
  89. sys.stderr = orig_stderr
  90. def test_setattr(self):
  91. options = OptionParser()
  92. options.define("foo", default=1, type=int)
  93. options.foo = 2
  94. self.assertEqual(options.foo, 2)
  95. def test_setattr_type_check(self):
  96. # setattr requires that options be the right type and doesn't
  97. # parse from string formats.
  98. options = OptionParser()
  99. options.define("foo", default=1, type=int)
  100. with self.assertRaises(Error):
  101. options.foo = "2"
  102. def test_setattr_with_callback(self):
  103. values = [] # type: List[int]
  104. options = OptionParser()
  105. options.define("foo", default=1, type=int, callback=values.append)
  106. options.foo = 2
  107. self.assertEqual(values, [2])
  108. def _sample_options(self):
  109. options = OptionParser()
  110. options.define("a", default=1)
  111. options.define("b", default=2)
  112. return options
  113. def test_iter(self):
  114. options = self._sample_options()
  115. # OptionParsers always define 'help'.
  116. self.assertEqual(set(["a", "b", "help"]), set(iter(options)))
  117. def test_getitem(self):
  118. options = self._sample_options()
  119. self.assertEqual(1, options["a"])
  120. def test_setitem(self):
  121. options = OptionParser()
  122. options.define("foo", default=1, type=int)
  123. options["foo"] = 2
  124. self.assertEqual(options["foo"], 2)
  125. def test_items(self):
  126. options = self._sample_options()
  127. # OptionParsers always define 'help'.
  128. expected = [("a", 1), ("b", 2), ("help", options.help)]
  129. actual = sorted(options.items())
  130. self.assertEqual(expected, actual)
  131. def test_as_dict(self):
  132. options = self._sample_options()
  133. expected = {"a": 1, "b": 2, "help": options.help}
  134. self.assertEqual(expected, options.as_dict())
  135. def test_group_dict(self):
  136. options = OptionParser()
  137. options.define("a", default=1)
  138. options.define("b", group="b_group", default=2)
  139. frame = sys._getframe(0)
  140. this_file = frame.f_code.co_filename
  141. self.assertEqual(set(["b_group", "", this_file]), options.groups())
  142. b_group_dict = options.group_dict("b_group")
  143. self.assertEqual({"b": 2}, b_group_dict)
  144. self.assertEqual({}, options.group_dict("nonexistent"))
  145. def test_mock_patch(self):
  146. # ensure that our setattr hooks don't interfere with mock.patch
  147. options = OptionParser()
  148. options.define("foo", default=1)
  149. options.parse_command_line(["main.py", "--foo=2"])
  150. self.assertEqual(options.foo, 2)
  151. with mock.patch.object(options.mockable(), "foo", 3):
  152. self.assertEqual(options.foo, 3)
  153. self.assertEqual(options.foo, 2)
  154. # Try nested patches mixed with explicit sets
  155. with mock.patch.object(options.mockable(), "foo", 4):
  156. self.assertEqual(options.foo, 4)
  157. options.foo = 5
  158. self.assertEqual(options.foo, 5)
  159. with mock.patch.object(options.mockable(), "foo", 6):
  160. self.assertEqual(options.foo, 6)
  161. self.assertEqual(options.foo, 5)
  162. self.assertEqual(options.foo, 2)
  163. def _define_options(self):
  164. options = OptionParser()
  165. options.define("str", type=str)
  166. options.define("basestring", type=basestring_type)
  167. options.define("int", type=int)
  168. options.define("float", type=float)
  169. options.define("datetime", type=datetime.datetime)
  170. options.define("timedelta", type=datetime.timedelta)
  171. options.define("email", type=Email)
  172. options.define("list-of-int", type=int, multiple=True)
  173. return options
  174. def _check_options_values(self, options):
  175. self.assertEqual(options.str, "asdf")
  176. self.assertEqual(options.basestring, "qwer")
  177. self.assertEqual(options.int, 42)
  178. self.assertEqual(options.float, 1.5)
  179. self.assertEqual(options.datetime, datetime.datetime(2013, 4, 28, 5, 16))
  180. self.assertEqual(options.timedelta, datetime.timedelta(seconds=45))
  181. self.assertEqual(options.email.value, "tornado@web.com")
  182. self.assertTrue(isinstance(options.email, Email))
  183. self.assertEqual(options.list_of_int, [1, 2, 3])
  184. def test_types(self):
  185. options = self._define_options()
  186. options.parse_command_line(
  187. [
  188. "main.py",
  189. "--str=asdf",
  190. "--basestring=qwer",
  191. "--int=42",
  192. "--float=1.5",
  193. "--datetime=2013-04-28 05:16",
  194. "--timedelta=45s",
  195. "--email=tornado@web.com",
  196. "--list-of-int=1,2,3",
  197. ]
  198. )
  199. self._check_options_values(options)
  200. def test_types_with_conf_file(self):
  201. for config_file_name in (
  202. "options_test_types.cfg",
  203. "options_test_types_str.cfg",
  204. ):
  205. options = self._define_options()
  206. options.parse_config_file(
  207. os.path.join(os.path.dirname(__file__), config_file_name)
  208. )
  209. self._check_options_values(options)
  210. def test_multiple_string(self):
  211. options = OptionParser()
  212. options.define("foo", type=str, multiple=True)
  213. options.parse_command_line(["main.py", "--foo=a,b,c"])
  214. self.assertEqual(options.foo, ["a", "b", "c"])
  215. def test_multiple_int(self):
  216. options = OptionParser()
  217. options.define("foo", type=int, multiple=True)
  218. options.parse_command_line(["main.py", "--foo=1,3,5:7"])
  219. self.assertEqual(options.foo, [1, 3, 5, 6, 7])
  220. def test_error_redefine(self):
  221. options = OptionParser()
  222. options.define("foo")
  223. with self.assertRaises(Error) as cm:
  224. options.define("foo")
  225. self.assertRegexpMatches(str(cm.exception), "Option.*foo.*already defined")
  226. def test_error_redefine_underscore(self):
  227. # Ensure that the dash/underscore normalization doesn't
  228. # interfere with the redefinition error.
  229. tests = [
  230. ("foo-bar", "foo-bar"),
  231. ("foo_bar", "foo_bar"),
  232. ("foo-bar", "foo_bar"),
  233. ("foo_bar", "foo-bar"),
  234. ]
  235. for a, b in tests:
  236. with subTest(self, a=a, b=b):
  237. options = OptionParser()
  238. options.define(a)
  239. with self.assertRaises(Error) as cm:
  240. options.define(b)
  241. self.assertRegexpMatches(
  242. str(cm.exception), "Option.*foo.bar.*already defined"
  243. )
  244. def test_dash_underscore_cli(self):
  245. # Dashes and underscores should be interchangeable.
  246. for defined_name in ["foo-bar", "foo_bar"]:
  247. for flag in ["--foo-bar=a", "--foo_bar=a"]:
  248. options = OptionParser()
  249. options.define(defined_name)
  250. options.parse_command_line(["main.py", flag])
  251. # Attr-style access always uses underscores.
  252. self.assertEqual(options.foo_bar, "a")
  253. # Dict-style access allows both.
  254. self.assertEqual(options["foo-bar"], "a")
  255. self.assertEqual(options["foo_bar"], "a")
  256. def test_dash_underscore_file(self):
  257. # No matter how an option was defined, it can be set with underscores
  258. # in a config file.
  259. for defined_name in ["foo-bar", "foo_bar"]:
  260. options = OptionParser()
  261. options.define(defined_name)
  262. options.parse_config_file(
  263. os.path.join(os.path.dirname(__file__), "options_test.cfg")
  264. )
  265. self.assertEqual(options.foo_bar, "a")
  266. def test_dash_underscore_introspection(self):
  267. # Original names are preserved in introspection APIs.
  268. options = OptionParser()
  269. options.define("with-dash", group="g")
  270. options.define("with_underscore", group="g")
  271. all_options = ["help", "with-dash", "with_underscore"]
  272. self.assertEqual(sorted(options), all_options)
  273. self.assertEqual(sorted(k for (k, v) in options.items()), all_options)
  274. self.assertEqual(sorted(options.as_dict().keys()), all_options)
  275. self.assertEqual(
  276. sorted(options.group_dict("g")), ["with-dash", "with_underscore"]
  277. )
  278. # --help shows CLI-style names with dashes.
  279. buf = StringIO()
  280. options.print_help(buf)
  281. self.assertIn("--with-dash", buf.getvalue())
  282. self.assertIn("--with-underscore", buf.getvalue())