options_test.py 12 KB

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