""" Tests for traitlets.config.application.Application """ # Copyright (c) IPython Development Team. # Distributed under the terms of the Modified BSD License. from __future__ import annotations import contextlib import io import json import logging import os import sys import typing as t from io import StringIO from tempfile import TemporaryDirectory from unittest import TestCase, mock import pytest from traitlets import Bool, Bytes, Dict, HasTraits, Integer, List, Set, Tuple, Unicode from traitlets.config.application import Application from traitlets.config.configurable import Configurable from traitlets.config.loader import Config, KVArgParseConfigLoader from traitlets.tests.utils import check_help_all_output, check_help_output, get_output_error_code pjoin = os.path.join class Foo(Configurable): i = Integer( 0, help=""" The integer i. Details about i. """, ).tag(config=True) j = Integer(1, help="The integer j.").tag(config=True) name = Unicode("Brian", help="First name.").tag(config=True) la = List([]).tag(config=True) li = List(Integer()).tag(config=True) fdict = Dict().tag(config=True, multiplicity="+") class Bar(Configurable): b = Integer(0, help="The integer b.").tag(config=True) enabled = Bool(True, help="Enable bar.").tag(config=True) tb = Tuple(()).tag(config=True, multiplicity="*") aset = Set().tag(config=True, multiplicity="+") bdict = Dict().tag(config=True) idict = Dict(value_trait=Integer()).tag(config=True) key_dict = Dict(per_key_traits={"i": Integer(), "b": Bytes()}).tag(config=True) class MyApp(Application): name = Unicode("myapp") running = Bool(False, help="Is the app running?").tag(config=True) classes = List([Bar, Foo]) # type:ignore config_file = Unicode("", help="Load this config file").tag(config=True) warn_tpyo = Unicode( "yes the name is wrong on purpose", config=True, help="Should print a warning if `MyApp.warn-typo=...` command is passed", ) aliases: t.Dict[t.Any, t.Any] = {} aliases.update(Application.aliases) aliases.update( { ("fooi", "i"): "Foo.i", ("j", "fooj"): ("Foo.j", "`j` terse help msg"), "name": "Foo.name", "la": "Foo.la", "li": "Foo.li", "tb": "Bar.tb", "D": "Bar.bdict", "enabled": "Bar.enabled", "enable": "Bar.enabled", "log-level": "Application.log_level", } ) flags: t.Dict[t.Any, t.Any] = {} flags.update(Application.flags) flags.update( { ("enable", "e"): ({"Bar": {"enabled": True}}, "Set Bar.enabled to True"), ("d", "disable"): ({"Bar": {"enabled": False}}, "Set Bar.enabled to False"), "crit": ({"Application": {"log_level": logging.CRITICAL}}, "set level=CRITICAL"), } ) def init_foo(self): self.foo = Foo(parent=self) def init_bar(self): self.bar = Bar(parent=self) def class_to_names(classes): return [klass.__name__ for klass in classes] class TestApplication(TestCase): def test_log(self): stream = StringIO() app = MyApp(log_level=logging.INFO) handler = logging.StreamHandler(stream) # trigger reconstruction of the log formatter app.log_format = "%(message)s" app.log_datefmt = "%Y-%m-%d %H:%M" app.log.handlers = [handler] app.log.info("hello") assert "hello" in stream.getvalue() def test_no_eval_cli_text(self): app = MyApp() app.initialize(["--Foo.name=1"]) app.init_foo() assert app.foo.name == "1" def test_basic(self): app = MyApp() self.assertEqual(app.name, "myapp") self.assertEqual(app.running, False) self.assertEqual(app.classes, [MyApp, Bar, Foo]) # type:ignore self.assertEqual(app.config_file, "") def test_app_name_set_via_constructor(self): app = MyApp(name="set_via_constructor") assert app.name == "set_via_constructor" def test_mro_discovery(self): app = MyApp() self.assertSequenceEqual( class_to_names(app._classes_with_config_traits()), ["Application", "MyApp", "Bar", "Foo"], ) self.assertSequenceEqual( class_to_names(app._classes_inc_parents()), [ "Configurable", "LoggingConfigurable", "SingletonConfigurable", "Application", "MyApp", "Bar", "Foo", ], ) self.assertSequenceEqual( class_to_names(app._classes_with_config_traits([Application])), ["Application"] ) self.assertSequenceEqual( class_to_names(app._classes_inc_parents([Application])), ["Configurable", "LoggingConfigurable", "SingletonConfigurable", "Application"], ) self.assertSequenceEqual(class_to_names(app._classes_with_config_traits([Foo])), ["Foo"]) self.assertSequenceEqual( class_to_names(app._classes_inc_parents([Bar])), ["Configurable", "Bar"] ) class MyApp2(Application): # no defined `classes` attr pass self.assertSequenceEqual(class_to_names(app._classes_with_config_traits([Foo])), ["Foo"]) self.assertSequenceEqual( class_to_names(app._classes_inc_parents([Bar])), ["Configurable", "Bar"] ) def test_config(self): app = MyApp() app.parse_command_line( [ "--i=10", "--Foo.j=10", "--enable=False", "--log-level=50", ] ) config = app.config print(config) self.assertEqual(config.Foo.i, 10) self.assertEqual(config.Foo.j, 10) self.assertEqual(config.Bar.enabled, False) self.assertEqual(config.MyApp.log_level, 50) def test_config_seq_args(self): app = MyApp() app.parse_command_line( "--li 1 --li 3 --la 1 --tb AB 2 --Foo.la=ab --Bar.aset S1 --Bar.aset S2 --Bar.aset S1".split() ) assert app.extra_args == ["2"] config = app.config assert config.Foo.li == [1, 3] assert config.Foo.la == ["1", "ab"] assert config.Bar.tb == ("AB",) self.assertEqual(config.Bar.aset, {"S1", "S2"}) app.init_foo() assert app.foo.li == [1, 3] assert app.foo.la == ["1", "ab"] app.init_bar() self.assertEqual(app.bar.aset, {"S1", "S2"}) assert app.bar.tb == ("AB",) def test_config_dict_args(self): app = MyApp() app.parse_command_line( "--Foo.fdict a=1 --Foo.fdict b=b --Foo.fdict c=3 " "--Bar.bdict k=1 -D=a=b -D 22=33 " "--Bar.idict k=1 --Bar.idict b=2 --Bar.idict c=3 ".split() ) fdict = {"a": "1", "b": "b", "c": "3"} bdict = {"k": "1", "a": "b", "22": "33"} idict = {"k": 1, "b": 2, "c": 3} config = app.config assert config.Bar.idict == idict self.assertDictEqual(config.Foo.fdict, fdict) self.assertDictEqual(config.Bar.bdict, bdict) app.init_foo() self.assertEqual(app.foo.fdict, fdict) app.init_bar() assert app.bar.idict == idict self.assertEqual(app.bar.bdict, bdict) def test_config_propagation(self): app = MyApp() app.parse_command_line(["--i=10", "--Foo.j=10", "--enable=False", "--log-level=50"]) app.init_foo() app.init_bar() self.assertEqual(app.foo.i, 10) self.assertEqual(app.foo.j, 10) self.assertEqual(app.bar.enabled, False) def test_cli_priority(self): """Test that loading config files does not override CLI options""" name = "config.py" class TestApp(Application): value = Unicode().tag(config=True) config_file_loaded = Bool().tag(config=True) aliases = {"v": "TestApp.value"} app = TestApp() with TemporaryDirectory() as td: config_file = pjoin(td, name) with open(config_file, "w") as f: f.writelines( ["c.TestApp.value = 'config file'\n", "c.TestApp.config_file_loaded = True\n"] ) app.parse_command_line(["--v=cli"]) assert "value" in app.config.TestApp assert app.config.TestApp.value == "cli" assert app.value == "cli" app.load_config_file(name, path=[td]) assert app.config_file_loaded assert app.config.TestApp.value == "cli" assert app.value == "cli" def test_ipython_cli_priority(self): # this test is almost entirely redundant with above, # but we can keep it around in case of subtle issues creeping into # the exact sequence IPython follows. name = "config.py" class TestApp(Application): value = Unicode().tag(config=True) config_file_loaded = Bool().tag(config=True) aliases = {"v": ("TestApp.value", "some help")} app = TestApp() with TemporaryDirectory() as td: config_file = pjoin(td, name) with open(config_file, "w") as f: f.writelines( ["c.TestApp.value = 'config file'\n", "c.TestApp.config_file_loaded = True\n"] ) # follow IPython's config-loading sequence to ensure CLI priority is preserved app.parse_command_line(["--v=cli"]) # this is where IPython makes a mistake: # it assumes app.config will not be modified, # and storing a reference is storing a copy cli_config = app.config assert "value" in app.config.TestApp assert app.config.TestApp.value == "cli" assert app.value == "cli" app.load_config_file(name, path=[td]) assert app.config_file_loaded # enforce cl-opts override config file opts: # this is where IPython makes a mistake: it assumes # that cl_config is a different object, but it isn't. app.update_config(cli_config) assert app.config.TestApp.value == "cli" assert app.value == "cli" def test_cli_allow_none(self): class App(Application): aliases = {"opt": "App.opt"} opt = Unicode(allow_none=True, config=True) app = App() app.parse_command_line(["--opt=None"]) assert app.opt is None def test_flags(self): app = MyApp() app.parse_command_line(["--disable"]) app.init_bar() self.assertEqual(app.bar.enabled, False) app = MyApp() app.parse_command_line(["-d"]) app.init_bar() self.assertEqual(app.bar.enabled, False) app = MyApp() app.parse_command_line(["--enable"]) app.init_bar() self.assertEqual(app.bar.enabled, True) app = MyApp() app.parse_command_line(["-e"]) app.init_bar() self.assertEqual(app.bar.enabled, True) def test_flags_help_msg(self): app = MyApp() stdout = io.StringIO() with contextlib.redirect_stdout(stdout): app.print_flag_help() hmsg = stdout.getvalue() self.assertRegex(hmsg, "(? 2 aliases for the same argument class TestMultiAliasApp(Application): foo = Integer(config=True) aliases = {("f", "bar", "qux"): "TestMultiAliasApp.foo"} app = TestMultiAliasApp() app.parse_command_line(["-f", "3"]) self.assertEqual(app.foo, 3) app = TestMultiAliasApp() app.parse_command_line(["--bar", "4"]) self.assertEqual(app.foo, 4) app = TestMultiAliasApp() app.parse_command_line(["--qux", "5"]) self.assertEqual(app.foo, 5) def test_aliases_help_msg(self): app = MyApp() stdout = io.StringIO() with contextlib.redirect_stdout(stdout): app.print_alias_help() hmsg = stdout.getvalue() self.assertRegex(hmsg, "(? 0 def test_logging_config(tmp_path, capsys): """We should be able to configure additional log handlers.""" log_file = tmp_path / "log_file" app = Application( logging_config={ "version": 1, "handlers": { "file": { "class": "logging.FileHandler", "level": "DEBUG", "filename": str(log_file), }, }, "loggers": { "Application": { "level": "DEBUG", "handlers": ["console", "file"], }, }, } ) # the default "console" handler + our new "file" handler assert len(app.log.handlers) == 2 # log a couple of messages app.log.info("info") app.log.warning("warn") # test that log messages get written to the file with open(log_file) as log_handle: assert log_handle.read() == "info\nwarn\n" # test that log messages get written to stderr (default console handler) assert capsys.readouterr().err == "[Application] WARNING | warn\n" def test_get_default_logging_config_pythonw(monkeypatch): """Ensure logging is correctly disabled for pythonw usage.""" monkeypatch.setattr("traitlets.config.application.IS_PYTHONW", True) config = Application().get_default_logging_config() assert "handlers" not in config assert "loggers" not in config monkeypatch.setattr("traitlets.config.application.IS_PYTHONW", False) config = Application().get_default_logging_config() assert "handlers" in config assert "loggers" in config @pytest.fixture() def caplogconfig(monkeypatch): """Capture logging config events for DictConfigurator objects. This suppresses the event (so the configuration doesn't happen). Returns a list of (args, kwargs). """ calls = [] def _configure(*args, **kwargs): nonlocal calls calls.append((args, kwargs)) monkeypatch.setattr( "logging.config.DictConfigurator.configure", _configure, ) return calls @pytest.mark.skipif(sys.implementation.name == "pypy", reason="Test does not work on pypy") def test_logging_teardown_on_error(capsys, caplogconfig): """Ensure we don't try to open logs in order to close them (See #722). If you try to configure logging handlers whilst Python is shutting down you may get traceback. """ # create and destroy an app (without configuring logging) # (ensure that the logs were not configured) app = Application() del app assert len(caplogconfig) == 0 # logging was not configured out, err = capsys.readouterr() assert "Traceback" not in err # ensure that the logs would have been configured otherwise # (to prevent this test from yielding false-negatives) app = Application() app._logging_configured = True # make it look like logging was configured del app assert len(caplogconfig) == 1 # logging was configured if __name__ == "__main__": # for test_help_output: MyApp.launch_instance()