test_application.py 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379
  1. # coding: utf-8
  2. """
  3. Tests for traitlets.config.application.Application
  4. """
  5. # Copyright (c) IPython Development Team.
  6. # Distributed under the terms of the Modified BSD License.
  7. import json
  8. import logging
  9. import os
  10. from io import StringIO
  11. from unittest import TestCase
  12. try:
  13. from unittest import mock
  14. except ImportError:
  15. import mock
  16. pjoin = os.path.join
  17. from pytest import mark
  18. from traitlets.config.configurable import Configurable
  19. from traitlets.config.loader import Config
  20. from traitlets.tests.utils import check_help_output, check_help_all_output
  21. from traitlets.config.application import (
  22. Application
  23. )
  24. from ipython_genutils.tempdir import TemporaryDirectory
  25. from traitlets.traitlets import (
  26. Bool, Unicode, Integer, List, Dict
  27. )
  28. class Foo(Configurable):
  29. i = Integer(0, help="The integer i.").tag(config=True)
  30. j = Integer(1, help="The integer j.").tag(config=True)
  31. name = Unicode(u'Brian', help="First name.").tag(config=True)
  32. class Bar(Configurable):
  33. b = Integer(0, help="The integer b.").tag(config=True)
  34. enabled = Bool(True, help="Enable bar.").tag(config=True)
  35. class MyApp(Application):
  36. name = Unicode(u'myapp')
  37. running = Bool(False, help="Is the app running?").tag(config=True)
  38. classes = List([Bar, Foo])
  39. config_file = Unicode(u'', help="Load this config file").tag(config=True)
  40. warn_tpyo = Unicode(u"yes the name is wrong on purpose", config=True,
  41. help="Should print a warning if `MyApp.warn-typo=...` command is passed")
  42. aliases = Dict({
  43. 'i' : 'Foo.i',
  44. 'j' : 'Foo.j',
  45. 'name' : 'Foo.name',
  46. 'enabled' : 'Bar.enabled',
  47. 'log-level' : 'Application.log_level',
  48. })
  49. flags = Dict(dict(enable=({'Bar': {'enabled' : True}}, "Set Bar.enabled to True"),
  50. disable=({'Bar': {'enabled' : False}}, "Set Bar.enabled to False"),
  51. crit=({'Application' : {'log_level' : logging.CRITICAL}},
  52. "set level=CRITICAL"),
  53. ))
  54. def init_foo(self):
  55. self.foo = Foo(parent=self)
  56. def init_bar(self):
  57. self.bar = Bar(parent=self)
  58. class TestApplication(TestCase):
  59. def test_log(self):
  60. stream = StringIO()
  61. app = MyApp(log_level=logging.INFO)
  62. handler = logging.StreamHandler(stream)
  63. # trigger reconstruction of the log formatter
  64. app.log.handlers = [handler]
  65. app.log_format = "%(message)s"
  66. app.log_datefmt = "%Y-%m-%d %H:%M"
  67. app.log.info("hello")
  68. assert "hello" in stream.getvalue()
  69. def test_basic(self):
  70. app = MyApp()
  71. self.assertEqual(app.name, u'myapp')
  72. self.assertEqual(app.running, False)
  73. self.assertEqual(app.classes, [MyApp,Bar,Foo])
  74. self.assertEqual(app.config_file, u'')
  75. def test_config(self):
  76. app = MyApp()
  77. app.parse_command_line(["--i=10","--Foo.j=10","--enabled=False","--log-level=50"])
  78. config = app.config
  79. self.assertEqual(config.Foo.i, 10)
  80. self.assertEqual(config.Foo.j, 10)
  81. self.assertEqual(config.Bar.enabled, False)
  82. self.assertEqual(config.MyApp.log_level,50)
  83. def test_config_propagation(self):
  84. app = MyApp()
  85. app.parse_command_line(["--i=10","--Foo.j=10","--enabled=False","--log-level=50"])
  86. app.init_foo()
  87. app.init_bar()
  88. self.assertEqual(app.foo.i, 10)
  89. self.assertEqual(app.foo.j, 10)
  90. self.assertEqual(app.bar.enabled, False)
  91. def test_cli_priority(self):
  92. """Test that loading config files does not override CLI options"""
  93. name = 'config.py'
  94. class TestApp(Application):
  95. value = Unicode().tag(config=True)
  96. config_file_loaded = Bool().tag(config=True)
  97. aliases = {'v': 'TestApp.value'}
  98. app = TestApp()
  99. with TemporaryDirectory() as td:
  100. config_file = pjoin(td, name)
  101. with open(config_file, 'w') as f:
  102. f.writelines([
  103. "c.TestApp.value = 'config file'\n",
  104. "c.TestApp.config_file_loaded = True\n"
  105. ])
  106. app.parse_command_line(['--v=cli'])
  107. assert 'value' in app.config.TestApp
  108. assert app.config.TestApp.value == 'cli'
  109. assert app.value == 'cli'
  110. app.load_config_file(name, path=[td])
  111. assert app.config_file_loaded
  112. assert app.config.TestApp.value == 'cli'
  113. assert app.value == 'cli'
  114. def test_ipython_cli_priority(self):
  115. # this test is almost entirely redundant with above,
  116. # but we can keep it around in case of subtle issues creeping into
  117. # the exact sequence IPython follows.
  118. name = 'config.py'
  119. class TestApp(Application):
  120. value = Unicode().tag(config=True)
  121. config_file_loaded = Bool().tag(config=True)
  122. aliases = {'v': 'TestApp.value'}
  123. app = TestApp()
  124. with TemporaryDirectory() as td:
  125. config_file = pjoin(td, name)
  126. with open(config_file, 'w') as f:
  127. f.writelines([
  128. "c.TestApp.value = 'config file'\n",
  129. "c.TestApp.config_file_loaded = True\n"
  130. ])
  131. # follow IPython's config-loading sequence to ensure CLI priority is preserved
  132. app.parse_command_line(['--v=cli'])
  133. # this is where IPython makes a mistake:
  134. # it assumes app.config will not be modified,
  135. # and storing a reference is storing a copy
  136. cli_config = app.config
  137. assert 'value' in app.config.TestApp
  138. assert app.config.TestApp.value == 'cli'
  139. assert app.value == 'cli'
  140. app.load_config_file(name, path=[td])
  141. assert app.config_file_loaded
  142. # enforce cl-opts override config file opts:
  143. # this is where IPython makes a mistake: it assumes
  144. # that cl_config is a different object, but it isn't.
  145. app.update_config(cli_config)
  146. assert app.config.TestApp.value == 'cli'
  147. assert app.value == 'cli'
  148. def test_flags(self):
  149. app = MyApp()
  150. app.parse_command_line(["--disable"])
  151. app.init_bar()
  152. self.assertEqual(app.bar.enabled, False)
  153. app.parse_command_line(["--enable"])
  154. app.init_bar()
  155. self.assertEqual(app.bar.enabled, True)
  156. def test_aliases(self):
  157. app = MyApp()
  158. app.parse_command_line(["--i=5", "--j=10"])
  159. app.init_foo()
  160. self.assertEqual(app.foo.i, 5)
  161. app.init_foo()
  162. self.assertEqual(app.foo.j, 10)
  163. def test_flag_clobber(self):
  164. """test that setting flags doesn't clobber existing settings"""
  165. app = MyApp()
  166. app.parse_command_line(["--Bar.b=5", "--disable"])
  167. app.init_bar()
  168. self.assertEqual(app.bar.enabled, False)
  169. self.assertEqual(app.bar.b, 5)
  170. app.parse_command_line(["--enable", "--Bar.b=10"])
  171. app.init_bar()
  172. self.assertEqual(app.bar.enabled, True)
  173. self.assertEqual(app.bar.b, 10)
  174. def test_warn_autocorrect(self):
  175. stream = StringIO()
  176. app = MyApp(log_level=logging.INFO)
  177. app.log.handlers = [logging.StreamHandler(stream)]
  178. cfg = Config()
  179. cfg.MyApp.warn_typo = "WOOOO"
  180. app.config = cfg
  181. self.assertIn("warn_typo", stream.getvalue())
  182. self.assertIn("warn_tpyo", stream.getvalue())
  183. def test_flatten_flags(self):
  184. cfg = Config()
  185. cfg.MyApp.log_level = logging.WARN
  186. app = MyApp()
  187. app.update_config(cfg)
  188. self.assertEqual(app.log_level, logging.WARN)
  189. self.assertEqual(app.config.MyApp.log_level, logging.WARN)
  190. app.initialize(["--crit"])
  191. self.assertEqual(app.log_level, logging.CRITICAL)
  192. # this would be app.config.Application.log_level if it failed:
  193. self.assertEqual(app.config.MyApp.log_level, logging.CRITICAL)
  194. def test_flatten_aliases(self):
  195. cfg = Config()
  196. cfg.MyApp.log_level = logging.WARN
  197. app = MyApp()
  198. app.update_config(cfg)
  199. self.assertEqual(app.log_level, logging.WARN)
  200. self.assertEqual(app.config.MyApp.log_level, logging.WARN)
  201. app.initialize(["--log-level", "CRITICAL"])
  202. self.assertEqual(app.log_level, logging.CRITICAL)
  203. # this would be app.config.Application.log_level if it failed:
  204. self.assertEqual(app.config.MyApp.log_level, "CRITICAL")
  205. def test_extra_args(self):
  206. app = MyApp()
  207. app.parse_command_line(["--Bar.b=5", 'extra', "--disable", 'args'])
  208. app.init_bar()
  209. self.assertEqual(app.bar.enabled, False)
  210. self.assertEqual(app.bar.b, 5)
  211. self.assertEqual(app.extra_args, ['extra', 'args'])
  212. app = MyApp()
  213. app.parse_command_line(["--Bar.b=5", '--', 'extra', "--disable", 'args'])
  214. app.init_bar()
  215. self.assertEqual(app.bar.enabled, True)
  216. self.assertEqual(app.bar.b, 5)
  217. self.assertEqual(app.extra_args, ['extra', '--disable', 'args'])
  218. def test_unicode_argv(self):
  219. app = MyApp()
  220. app.parse_command_line(['ünîcødé'])
  221. def test_document_config_option(self):
  222. app = MyApp()
  223. app.document_config_options()
  224. def test_generate_config_file(self):
  225. app = MyApp()
  226. assert 'The integer b.' in app.generate_config_file()
  227. def test_generate_config_file_classes_to_include(self):
  228. class NoTraits(Foo, Bar):
  229. pass
  230. app = MyApp()
  231. app.classes.append(NoTraits)
  232. conf_txt = app.generate_config_file()
  233. self.assertIn('The integer b.', conf_txt)
  234. self.assertIn('# Bar(Configurable)', conf_txt)
  235. self.assertIn('# Foo(Configurable)', conf_txt)
  236. self.assertNotIn('# Configurable', conf_txt)
  237. self.assertIn('# NoTraits(Foo,Bar)', conf_txt)
  238. def test_multi_file(self):
  239. app = MyApp()
  240. app.log = logging.getLogger()
  241. name = 'config.py'
  242. with TemporaryDirectory('_1') as td1:
  243. with open(pjoin(td1, name), 'w') as f1:
  244. f1.write("get_config().MyApp.Bar.b = 1")
  245. with TemporaryDirectory('_2') as td2:
  246. with open(pjoin(td2, name), 'w') as f2:
  247. f2.write("get_config().MyApp.Bar.b = 2")
  248. app.load_config_file(name, path=[td2, td1])
  249. app.init_bar()
  250. self.assertEqual(app.bar.b, 2)
  251. app.load_config_file(name, path=[td1, td2])
  252. app.init_bar()
  253. self.assertEqual(app.bar.b, 1)
  254. @mark.skipif(not hasattr(TestCase, 'assertLogs'), reason='requires TestCase.assertLogs')
  255. def test_log_collisions(self):
  256. app = MyApp()
  257. app.log = logging.getLogger()
  258. app.log.setLevel(logging.INFO)
  259. name = 'config'
  260. with TemporaryDirectory('_1') as td:
  261. with open(pjoin(td, name + '.py'), 'w') as f:
  262. f.write("get_config().Bar.b = 1")
  263. with open(pjoin(td, name + '.json'), 'w') as f:
  264. json.dump({
  265. 'Bar': {
  266. 'b': 2
  267. }
  268. }, f)
  269. with self.assertLogs(app.log, logging.WARNING) as captured:
  270. app.load_config_file(name, path=[td])
  271. app.init_bar()
  272. assert app.bar.b == 2
  273. output = '\n'.join(captured.output)
  274. assert 'Collision' in output
  275. assert '1 ignored, using 2' in output
  276. assert pjoin(td, name + '.py') in output
  277. assert pjoin(td, name + '.json') in output
  278. @mark.skipif(not hasattr(TestCase, 'assertLogs'), reason='requires TestCase.assertLogs')
  279. def test_log_bad_config(self):
  280. app = MyApp()
  281. app.log = logging.getLogger()
  282. name = 'config.py'
  283. with TemporaryDirectory() as td:
  284. with open(pjoin(td, name), 'w') as f:
  285. f.write("syntax error()")
  286. with self.assertLogs(app.log, logging.ERROR) as captured:
  287. app.load_config_file(name, path=[td])
  288. output = '\n'.join(captured.output)
  289. self.assertIn('SyntaxError', output)
  290. def test_raise_on_bad_config(self):
  291. app = MyApp()
  292. app.raise_config_file_errors = True
  293. app.log = logging.getLogger()
  294. name = 'config.py'
  295. with TemporaryDirectory() as td:
  296. with open(pjoin(td, name), 'w') as f:
  297. f.write("syntax error()")
  298. with self.assertRaises(SyntaxError):
  299. app.load_config_file(name, path=[td])
  300. class DeprecatedApp(Application):
  301. override_called = False
  302. parent_called = False
  303. def _config_changed(self, name, old, new):
  304. self.override_called = True
  305. def _capture(*args):
  306. self.parent_called = True
  307. with mock.patch.object(self.log, 'debug', _capture):
  308. super(DeprecatedApp, self)._config_changed(name, old, new)
  309. def test_deprecated_notifier():
  310. app = DeprecatedApp()
  311. assert not app.override_called
  312. assert not app.parent_called
  313. app.config = Config({'A': {'b': 'c'}})
  314. assert app.override_called
  315. assert app.parent_called
  316. def test_help_output():
  317. check_help_output(__name__)
  318. check_help_all_output(__name__)
  319. if __name__ == '__main__':
  320. # for test_help_output:
  321. MyApp.launch_instance()