You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@superset.apache.org by be...@apache.org on 2024/02/08 22:33:13 UTC

(superset) branch master updated: chore: prevent prophet from logging non-errors as errors (#27053)

This is an automated email from the ASF dual-hosted git repository.

beto pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/superset.git


The following commit(s) were added to refs/heads/master by this push:
     new cf84f36a6c chore: prevent prophet from logging non-errors as errors (#27053)
cf84f36a6c is described below

commit cf84f36a6cf3341db139fbf3457b91685bbe8486
Author: Beto Dealmeida <ro...@dealmeida.net>
AuthorDate: Thu Feb 8 17:33:07 2024 -0500

    chore: prevent prophet from logging non-errors as errors (#27053)
---
 superset/utils/decorators.py                    | 19 +++++++++++
 superset/utils/pandas_postprocessing/prophet.py |  7 ++--
 tests/unit_tests/utils/test_decorators.py       | 45 +++++++++++++++++++++++++
 3 files changed, 69 insertions(+), 2 deletions(-)

diff --git a/superset/utils/decorators.py b/superset/utils/decorators.py
index 43b6909caf..7e34b98360 100644
--- a/superset/utils/decorators.py
+++ b/superset/utils/decorators.py
@@ -191,3 +191,22 @@ def debounce(duration: float | int = 0.1) -> Callable[..., Any]:
 
 def on_security_exception(self: Any, ex: Exception) -> Response:
     return self.response(403, **{"message": utils.error_msg_from_exception(ex)})
+
+
+@contextmanager
+def suppress_logging(
+    logger_name: str | None = None,
+    new_level: int = logging.CRITICAL,
+) -> Iterator[None]:
+    """
+    Context manager to suppress logging during the execution of code block.
+
+    Use with caution and make sure you have the least amount of code inside it.
+    """
+    target_logger = logging.getLogger(logger_name)
+    original_level = target_logger.getEffectiveLevel()
+    target_logger.setLevel(new_level)
+    try:
+        yield
+    finally:
+        target_logger.setLevel(original_level)
diff --git a/superset/utils/pandas_postprocessing/prophet.py b/superset/utils/pandas_postprocessing/prophet.py
index 47d956fed5..a1c823ee2a 100644
--- a/superset/utils/pandas_postprocessing/prophet.py
+++ b/superset/utils/pandas_postprocessing/prophet.py
@@ -23,6 +23,7 @@ from pandas import DataFrame
 
 from superset.exceptions import InvalidPostProcessingError
 from superset.utils.core import DTTM_ALIAS
+from superset.utils.decorators import suppress_logging
 from superset.utils.pandas_postprocessing.utils import PROPHET_TIME_GRAIN_MAP
 
 
@@ -52,8 +53,10 @@ def _prophet_fit_and_predict(  # pylint: disable=too-many-arguments
     Fit a prophet model and return a DataFrame with predicted results.
     """
     try:
-        # pylint: disable=import-outside-toplevel
-        from prophet import Prophet
+        # `prophet` complains about `plotly` not being installed
+        with suppress_logging("prophet.plot"):
+            # pylint: disable=import-outside-toplevel
+            from prophet import Prophet
 
         prophet_logger = logging.getLogger("prophet.plot")
         prophet_logger.setLevel(logging.CRITICAL)
diff --git a/tests/unit_tests/utils/test_decorators.py b/tests/unit_tests/utils/test_decorators.py
index 6cfe55b641..0a622f4bce 100644
--- a/tests/unit_tests/utils/test_decorators.py
+++ b/tests/unit_tests/utils/test_decorators.py
@@ -16,6 +16,7 @@
 # under the License.
 
 
+import logging
 import uuid
 from contextlib import nullcontext
 from inspect import isclass
@@ -249,3 +250,47 @@ def test_context_decorator(flask_g_mock) -> None:
 
     context_func_not_callable()
     assert flask_g_mock.logs_context == {}
+
+
+class ListHandler(logging.Handler):
+    """
+    Simple logging handler that stores records in a list.
+    """
+
+    def __init__(self, *args: Any, **kwargs: Any):
+        super().__init__(*args, **kwargs)
+        self.log_records: list[logging.LogRecord] = []
+
+    def emit(self, record: logging.LogRecord) -> None:
+        self.log_records.append(record)
+
+    def reset(self) -> None:
+        self.log_records = []
+
+
+def test_suppress_logging() -> None:
+    """
+    Test the `suppress_logging` decorator.
+    """
+    handler = ListHandler()
+    logger = logging.getLogger("test-logger")
+    logger.setLevel(logging.INFO)
+    logger.addHandler(handler)
+
+    def func() -> None:
+        logger.error("error")
+        logger.critical("critical")
+
+    func()
+    assert len(handler.log_records) == 2
+
+    handler.log_records = []
+    decorated = decorators.suppress_logging("test-logger")(func)
+    decorated()
+    assert len(handler.log_records) == 1
+    assert handler.log_records[0].levelname == "CRITICAL"
+
+    handler.log_records = []
+    decorated = decorators.suppress_logging("test-logger", logging.CRITICAL + 1)(func)
+    decorated()
+    assert len(handler.log_records) == 0