Repository: streamlit/streamlit

Issue:
Log levels not working
Is setting `--global.logLevel=DEBUG` working?




Discussion:
The bug is probably around these lines:

```py
# in file lib/streamlit/__init__.py

def _set_log_level():
    # TODO: Check if this function is being called? Put print() statement or use
    # a debugger (for example, PyCharm and VS Code have nice onces). If not, why?
    # TODO: If being called, then set_log_level may not be doing the right
    # thing! Why?
    _logger.set_log_level(_config.get_option("global.logLevel").upper())
    _logger.init_tornado_logs()
```
Looks like the config gets parsed (and `_set_log_level` gets called) before the cli arguments are parsed. 

Resolving patch:
diff --git a/lib/streamlit/__init__.py b/lib/streamlit/__init__.py
--- a/lib/streamlit/__init__.py
+++ b/lib/streamlit/__init__.py
@@ -124,7 +124,7 @@ def _set_log_level():
 # Make this file only depend on config option in an asynchronous manner. This
 # avoids a race condition when another file (such as a test file) tries to pass
 # in an alternative config.
-_config.on_config_parsed(_set_log_level)
+_config.on_config_parsed(_set_log_level, True)
 
 
 _main = _DeltaGenerator(container=_BlockPath_pb2.BlockPath.MAIN)
diff --git a/lib/streamlit/cli.py b/lib/streamlit/cli.py
--- a/lib/streamlit/cli.py
+++ b/lib/streamlit/cli.py
@@ -92,6 +92,11 @@ def _apply_config_options_from_cli(kwargs):
     config.toml file
 
     """
+    # Parse config files first before setting CLI args.
+    # Prevents CLI args from being overwritten
+    if not _config._config_file_has_been_parsed:
+        _config.parse_config_file()
+
     for config_option in kwargs:
         if kwargs[config_option] is not None:
             config_option_def_key = config_option.replace("_", ".")
@@ -102,6 +107,8 @@ def _apply_config_options_from_cli(kwargs):
                 "command-line argument or environment variable",
             )
 
+    _config._on_config_parsed.send()
+
 
 # Fetch remote file at url_path to script_path
 def _download_remote(script_path, url_path):
diff --git a/lib/streamlit/config.py b/lib/streamlit/config.py
--- a/lib/streamlit/config.py
+++ b/lib/streamlit/config.py
@@ -966,18 +966,18 @@ def _set_development_mode():
     development.is_development_mode = get_option("global.developmentMode")
 
 
-def on_config_parsed(func):
+def on_config_parsed(func, force_connect=False):
     """Wait for the config file to be parsed then call func.
 
     If the config file has already been parsed, just calls fun immediately.
 
     """
-    if _config_file_has_been_parsed:
-        func()
-    else:
+    if force_connect or not _config_file_has_been_parsed:
         # weak=False, because we're using an anonymous lambda that
         # goes out of scope immediately.
         _on_config_parsed.connect(lambda _: func(), weak=False)
+    else:
+        func()
 
 
 # Run _check_conflicts only once the config file is parsed in order to avoid


Test patch:
diff --git a/lib/tests/streamlit/cli_test.py b/lib/tests/streamlit/cli_test.py
--- a/lib/tests/streamlit/cli_test.py
+++ b/lib/tests/streamlit/cli_test.py
@@ -28,6 +28,7 @@
 
 import streamlit
 from streamlit import cli
+from streamlit import config
 from streamlit.cli import _convert_config_option_to_click_option
 from streamlit.cli import _apply_config_options_from_cli
 from streamlit.ConfigOption import ConfigOption
@@ -40,6 +41,7 @@ def setUp(self):
         cli.name = "test"
         self.runner = CliRunner()
         streamlit._is_running_with_streamlit = False
+        patch.object(config._on_config_parsed, "send").start()
 
     def test_run_no_arguments(self):
         """streamlit run should fail if run with no arguments."""
@@ -175,8 +177,11 @@ def test_convert_config_option_to_click_option(self):
         self.assertEqual(result["description"], config_option.description)
         self.assertEqual(result["envvar"], "STREAMLIT_SERVER_CUSTOM_KEY")
 
+    @patch("streamlit.cli._config._on_config_parsed.send")
     @patch("streamlit.cli._config._set_option")
-    def test_apply_config_options_from_cli(self, patched__set_option):
+    def test_apply_config_options_from_cli(
+        self, patched__set_option, patched___send_on_config_parsed
+    ):
         """Test that _apply_config_options_from_cli parses the key properly and
         passes down the parameters.
         """
@@ -186,6 +191,7 @@ def test_apply_config_options_from_cli(self, patched__set_option):
             "server_headless": True,
             "browser_serverAddress": "localhost",
             "global_minCachedMessageSize": None,
+            "global_logLevel": "error",
         }
 
         _apply_config_options_from_cli(kwargs)
@@ -205,9 +211,15 @@ def test_apply_config_options_from_cli(self, patched__set_option):
                     "localhost",
                     "command-line argument or environment variable",
                 ),
+                mock.call(
+                    "global.logLevel",
+                    "error",
+                    "command-line argument or environment variable",
+                ),
             ],
             any_order=True,
         )
+        self.assertTrue(patched___send_on_config_parsed.called)
 
     def test_credentials_headless_no_config(self):
         """If headless mode and no config is present,
diff --git a/lib/tests/streamlit/config_test.py b/lib/tests/streamlit/config_test.py
--- a/lib/tests/streamlit/config_test.py
+++ b/lib/tests/streamlit/config_test.py
@@ -19,8 +19,10 @@
 import unittest
 
 import pytest
+from mock import MagicMock
 from mock import mock_open
 from mock import patch
+from parameterized import parameterized
 
 from streamlit import config
 from streamlit import env_util
@@ -510,6 +512,28 @@ def test_global_log_level(self):
         config.set_option("global.developmentMode", False)
         self.assertEqual("info", config.get_option("global.logLevel"))
 
+    @parameterized.expand([(True, True), (True, False), (False, False), (False, True)])
+    def test_on_config_parsed(self, config_parsed, connect_signal):
+        """Tests to make sure callback is handled properly based upon
+        _config_file_has_been_parsed and connect_signal."""
+
+        mock_callback = MagicMock(return_value=None)
+
+        with patch.object(config, "_config_file_has_been_parsed", new=config_parsed):
+            with patch.object(config._on_config_parsed, "connect") as patched_connect:
+                mock_callback.reset_mock()
+                config.on_config_parsed(mock_callback, connect_signal)
+
+                if connect_signal:
+                    patched_connect.assert_called_once()
+                    mock_callback.assert_not_called()
+                elif config_parsed:
+                    patched_connect.assert_not_called()
+                    mock_callback.assert_called_once()
+                else:
+                    patched_connect.assert_called_once()
+                    mock_callback.assert_not_called()
+
 
 class ConfigLoadingTest(unittest.TestCase):
     """Tests that involve loading the config.toml file."""