diff --git a/extensions/positron-python/python_files/positron/positron_ipykernel/positron_ipkernel.py b/extensions/positron-python/python_files/positron/positron_ipykernel/positron_ipkernel.py index 7d8162be5aa4..2f21952d9e5e 100644 --- a/extensions/positron-python/python_files/positron/positron_ipykernel/positron_ipkernel.py +++ b/extensions/positron-python/python_files/positron/positron_ipykernel/positron_ipkernel.py @@ -16,6 +16,7 @@ import traitlets from ipykernel.comm.manager import CommManager +from ipykernel.compiler import get_tmp_directory from ipykernel.ipkernel import IPythonKernel from ipykernel.kernelapp import IPKernelApp from ipykernel.zmqshell import ZMQDisplayPublisher, ZMQInteractiveShell @@ -190,6 +191,9 @@ def connection_show(self, line: str) -> None: _traceback_file_link_re = re.compile(r"^(File \x1b\[\d+;\d+m)(.+):(\d+)") +# keep reference to original showwarning +original_showwarning = warnings.showwarning + class PositronShell(ZMQInteractiveShell): kernel: PositronIPyKernel @@ -419,6 +423,8 @@ def __init__(self, **kwargs) -> None: # Register display publisher hooks self.shell.display_pub.register_hook(self.widget_hook) + warnings.showwarning = self._showwarning + # Ignore warnings that the user can't do anything about warnings.filterwarnings( "ignore", @@ -468,6 +474,29 @@ async def do_shutdown(self, restart: bool) -> JsonRecord: # type: ignore Report # points to the same underlying asyncio loop). return dict(status="ok", restart=restart) + # monkey patching warning.showwarning is recommended by the official documentation + # https://docs.python.org/3/library/warnings.html#warnings.showwarning + def _showwarning(self, message, category, filename, lineno, file=None, line=None): + # if coming from one of our files, log and don't send to user + positron_files_path = Path(__file__).parent + + if str(positron_files_path) in str(filename): + msg = f"{filename}-{lineno}: {category}: {message}" + logger.warning(msg) + return + + # Check if the filename refers to a cell in the Positron Console. + # We use the fact that ipykernel sets the filename to a path starting in the root temporary + # directory. We can't determine the full filename since it depends on the cell's code which + # is unknown at this point. See ipykernel.compiler.XCachingCompiler.get_code_name. + console_dir = get_tmp_directory() + if console_dir in str(filename): + filename = f"" + + msg = warnings.WarningMessage(message, category, filename, lineno, file, line) + + return original_showwarning(message, category, filename, lineno, file, line) # type: ignore reportAttributeAccessIssue + class PositronIPKernelApp(IPKernelApp): kernel: PositronIPyKernel diff --git a/extensions/positron-python/python_files/positron/positron_ipykernel/tests/test_positron_ipkernel.py b/extensions/positron-python/python_files/positron/positron_ipykernel/tests/test_positron_ipkernel.py index f264048fa797..4d4de9c45737 100644 --- a/extensions/positron-python/python_files/positron/positron_ipykernel/tests/test_positron_ipkernel.py +++ b/extensions/positron-python/python_files/positron/positron_ipykernel/tests/test_positron_ipkernel.py @@ -3,11 +3,14 @@ # import os +import logging from pathlib import Path from typing import Any, cast from unittest.mock import Mock +import pytest from IPython.utils.syspathcontext import prepended_to_syspath +from ipykernel.compiler import get_tmp_directory from positron_ipykernel.help import help from positron_ipykernel.session_mode import SessionMode @@ -23,6 +26,13 @@ # method with the expected arguments. The actual messages sent over # the comm are tested in the respective service tests. +logger = logging.getLogger(__name__) + + +@pytest.fixture +def warning_kwargs(): + return {"message": "this is a warning", "category": UserWarning, "lineno": 3} + def test_override_help(shell: PositronShell) -> None: """ @@ -308,3 +318,27 @@ def test_question_mark_help(shell: PositronShell, mock_help_service: Mock) -> No mock_help_service.show_help.assert_called_once_with( "positron_ipykernel.utils.positron_ipykernel_usage" ) + + +def test_console_warning(shell: PositronShell, warning_kwargs): + """ + Check message for warnings + """ + filename = get_tmp_directory() + os.sep + "12345678.py" + + with pytest.warns() as record: + shell.kernel._showwarning(filename=filename, **warning_kwargs) + + assert len(record) == 1 + assert record[0].filename == "" + assert record[0].message == "this is a warning" + + +def test_console_warning_logger(shell: PositronShell, caplog, warning_kwargs): + """ + Check that Positron files are sent to logs + """ + + with caplog.at_level(logging.WARNING): + shell.kernel._showwarning(filename=Path(__file__), **warning_kwargs) + assert "this is a warning" in caplog.text