Repository: ipython/traitlets (python)

Issue:
Logging errors on Application teardown
When using `traitlets` `5.2.0` to [test](https://dev.azure.com/conda-forge/feedstock-builds/_build/results?buildId=505255&view=logs&j=656edd35-690f-5c53-9ba3-09c10d0bea97&t=e5c8ab1d-8ff9-5cae-b332-e15ae582ed2d&l=2525) `jupyterlab` [`3.4.1`](https://github.com/conda-forge/jupyterlab-feedstock/pull/333):

```bash
2022-05-12T14:07:44.4320758Z + jupyter server extension list
2022-05-12T14:07:45.0244467Z Config dir: /home/conda/.jupyter
2022-05-12T14:07:45.0246143Z 
2022-05-12T14:07:45.5231596Z Config dir: $SRC/etc/jupyter
2022-05-12T14:07:45.5234278Z     jupyterlab enabled
2022-05-12T14:07:45.5235478Z     - Validating jupyterlab...
2022-05-12T14:07:45.5236604Z       jupyterlab 3.4.1 OK
2022-05-12T14:07:45.5237684Z     nbclassic enabled
2022-05-12T14:07:45.5238759Z     - Validating nbclassic...
2022-05-12T14:07:45.5239837Z       nbclassic  OK
2022-05-12T14:07:45.5240939Z     notebook_shim enabled
2022-05-12T14:07:45.5242021Z     - Validating notebook_shim...
2022-05-12T14:07:45.5243086Z       notebook_shim  OK
2022-05-12T14:07:45.5244219Z 
2022-05-12T14:07:45.5260353Z Config dir: /usr/local/etc/jupyter
2022-05-12T14:07:45.5262066Z 
2022-05-12T14:07:45.5601412Z Exception ignored in: <function Application.__del__ at 0x7fb85d73f760>
2022-05-12T14:07:45.5602339Z Traceback (most recent call last):
2022-05-12T14:07:45.5604678Z   File "$SRC/lib/python3.10/site-packages/traitlets/config/application.py", line 955, in __del__
2022-05-12T14:07:45.5607082Z   File "$SRC/lib/python3.10/site-packages/traitlets/config/application.py", line 945, in close_handlers
2022-05-12T14:07:45.5609436Z   File "$SRC/lib/python3.10/site-packages/traitlets/traitlets.py", line 683, in __get__
2022-05-12T14:07:45.5611694Z   File "$SRC/lib/python3.10/site-packages/traitlets/traitlets.py", line 657, in get
2022-05-12T14:07:45.5613965Z   File "$SRC/lib/python3.10/site-packages/traitlets/traitlets.py", line 1428, in _notify_observers
2022-05-12T14:07:45.5616640Z   File "$SRC/lib/python3.10/site-packages/traitlets/config/application.py", line 273, in _observe_logging_default
2022-05-12T14:07:45.5619022Z   File "$SRC/lib/python3.10/site-packages/traitlets/config/application.py", line 278, in _configure_logging
2022-05-12T14:07:45.5620968Z   File "$SRC/lib/python3.10/logging/config.py", line 810, in dictConfig
2022-05-12T14:07:45.5622663Z   File "$SRC/lib/python3.10/logging/config.py", line 547, in configure
2022-05-12T14:07:45.5624039Z ValueError: Unable to configure formatter 'console'
```

These errors are not present when testing (locally) with `traitlets 5.1.1`... will investigate further, but might be the weekend.

Pull request description:
logging: don't attempt to close handlers unless they have been opened
* Accessing `Application.log.handlers` causes them to be opened if not done already.
* Attempting to open log handlers whilst Python is shutting down causes traceback.
* Closes #722
* Traceback introduced by https://github.com/ipython/traitlets/pull/698 (apologies)

Patch:
diff --git a/docs/source/changelog.rst b/docs/source/changelog.rst
index 03b83e0..6b49549 100644
--- a/docs/source/changelog.rst
+++ b/docs/source/changelog.rst
@@ -4,6 +4,12 @@ Changes in Traitlets
 Traitlets 5.2
 -------------
 
+5.2.1
+*****
+
+  - logging: Don't attempt to close handlers unless they have been opened.
+    Fixes ``ValueError: Unable to configure formatter 'console'`` traceback.
+
 5.2.0
 *****
 
diff --git a/traitlets/config/application.py b/traitlets/config/application.py
index 02dc72e..e5a8686 100644
--- a/traitlets/config/application.py
+++ b/traitlets/config/application.py
@@ -276,6 +276,8 @@ class Application(SingletonConfigurable):
         config = self.get_default_logging_config()
         nested_update(config, self.logging_config or {})
         dictConfig(config)
+        # make a note that we have configured logging
+        self._logging_configured = True
 
     @default("log")
     def _log_default(self):
@@ -942,9 +944,14 @@ class Application(SingletonConfigurable):
         return "\n".join(lines)
 
     def close_handlers(self):
-        for handler in self.log.handlers:
-            with suppress(Exception):
-                handler.close()
+        if getattr(self, "_logging_configured", False):
+            # don't attempt to close handlers unless they have been opened
+            # (note accessing self.log.handlers will create handlers if they
+            # have not yet been initialised)
+            for handler in self.log.handlers:
+                with suppress(Exception):
+                    handler.close()
+            self._logging_configured = False
 
     def exit(self, exit_status=0):
         self.log.debug("Exiting application: %s" % self.name)


Test patch:
diff --git a/traitlets/config/tests/test_application.py b/traitlets/config/tests/test_application.py
index 3fc2862..4c64811 100644
--- a/traitlets/config/tests/test_application.py
+++ b/traitlets/config/tests/test_application.py
@@ -804,6 +804,50 @@ def test_logging_config(tmp_path, capsys):
     assert capsys.readouterr().err == "[Application] WARNING | warn\n"
 
 
+@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
+
+
+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()