diff --git a/ipylab/jupyterfrontend.py b/ipylab/jupyterfrontend.py index ea3e8f8..1740e58 100644 --- a/ipylab/jupyterfrontend.py +++ b/ipylab/jupyterfrontend.py @@ -23,7 +23,7 @@ from ipylab.dialog import Dialog from ipylab.ipylab import IpylabBase, Readonly from ipylab.launcher import Launcher -from ipylab.log import IpylabLogFormatter, IpylabLoggerAdapter, IpylabLogHandler, LogLevel +from ipylab.log import IpylabLogFormatter, IpylabLogHandler, LogLevel from ipylab.menu import ContextMenu, MainMenu from ipylab.notification import NotificationManager from ipylab.sessions import SessionManager @@ -71,7 +71,7 @@ class App(Ipylab): sessions = Readonly(SessionManager) console = Instance(ShellConnection, allow_none=True, read_only=True) - logging_handler = Instance(logging.Handler, allow_none=True, read_only=True) + logging_handler = Instance(IpylabLogHandler, read_only=True) active_namespace = Unicode("", read_only=True, help="name of the current namespace") selector = Unicode("", read_only=True, help="Selector class for context menus (css)") @@ -92,10 +92,9 @@ def close(self): @default("log") def _default_log(self): - log = IpylabLoggerAdapter(logging.getLogger("ipylab")) - if isinstance(self.logging_handler, logging.Handler): - log.logger.addHandler(self.logging_handler) - return log + log = logging.getLogger("ipylab") + self.logging_handler.set_as_handler(log) + return logging.LoggerAdapter(log) @default("logging_handler") def _default_logging_handler(self): @@ -218,10 +217,6 @@ async def _open_console(): return self.to_task(_open_console(), "Open console") - def toggle_log_console(self) -> Task[ShellConnection]: - # How can we check if the log console is open? - return self.commands.execute("logconsole:open", {"source": self.vpath}) - def shutdown_kernel(self, vpath: str | None = None): "Shutdown the kernel" return self.operation("shutdownKernel", {"vpath": vpath}) diff --git a/ipylab/lib.py b/ipylab/lib.py index 69f3ec5..6150f08 100644 --- a/ipylab/lib.py +++ b/ipylab/lib.py @@ -43,15 +43,18 @@ def launch_jupyterlab(): @hookimpl def on_error(obj: Ipylab, source: ErrorSource, error: Exception): - msg = f"{source} {error}" - obj.log.exception(msg, extra={"source": source}, exc_info=error) + obj.log.exception(str(source), exc_info=error) task = objects.get("error_task") if isinstance(task, Task): # Try to minimize the number of notifications. if not task.done(): return task.result().close() - a = NotifyAction(label="📝", caption="Toggle log console", callback=ipylab.app.toggle_log_console, keep_open=True) + msg = f"{ipylab.app} {source} {error} {obj=}" + if isinstance(obj, ipylab.ShellConnection): + a = NotifyAction(label="👀", caption="Activate", callback=obj.activate, keep_open=True) + else: + a = NotifyAction(label="📝", caption="Open console", callback=ipylab.app.open_console, keep_open=True) objects["error_task"] = ipylab.app.notification.notify(msg, type=ipylab.NotificationType.error, actions=[a]) diff --git a/ipylab/log.py b/ipylab/log.py index 942291c..e9235d7 100644 --- a/ipylab/log.py +++ b/ipylab/log.py @@ -109,7 +109,7 @@ class LogPayloadOutput(LogPayloadBase): class IpylabLogHandler(logging.Handler): - loggers: ClassVar[weakref.WeakSet[logging.Logger]] = weakref.WeakSet() + _loggers: ClassVar[weakref.WeakSet[logging.Logger]] = weakref.WeakSet() def __init__(self) -> None: ipylab.app.observe(self._observe_app_log_level, "logger_level") @@ -118,34 +118,30 @@ def __init__(self) -> None: def _observe_app_log_level(self, change: dict): level = LogLevel.to_numeric(change["new"]) self.setLevel(level) - for log in self.loggers: + for log in self._loggers: log.setLevel(level) def emit(self, record): - log = LogPayloadOutput( - type=LogTypes.output, level=LogLevel.to_level(record.levelno), data=self.parse_record(record) - ) - ipylab.app.send_log_message(log) - - def parse_record(self, record) -> OutputTypes: msg = self.format(record) - if record.levelno <= log_name_mappings[LogLevel.warning]: - return OutputStream(output_type="stream", type="stdout", text=msg) - if record.exc_info and record.exc_info[2]: - (etype, value, tb) = record.exc_info - stb = ipylab.app._ipy_shell.InteractiveTB.structured_traceback(etype, value, tb) # type: ignore # noqa: SLF001 - else: - value, stb = "", None - return OutputError(output_type="error", ename=str(value), evalue=msg, traceback=stb) - + data = OutputStream(output_type="stream", type="stdout", text=msg) + log = LogPayloadOutput(type=LogTypes.output, level=LogLevel.to_level(record.levelno), data=data) + ipylab.app.send_log_message(log) -class IpylabLoggerAdapter(logging.LoggerAdapter): - def __init__(self, logger: logging.Logger, extra=None): - logger.setLevel(LogLevel.to_numeric(ipylab.app.logger_level)) - IpylabLogHandler.loggers.add(logger) - super().__init__(logger, extra) + def set_as_handler(self, log: logging.Logger): + "Set this handler as a handler for log and keep the level in sync." + if log not in self._loggers: + log.addHandler(self) + log.setLevel(self.level) + self._loggers.add(log) class IpylabLogFormatter(logging.Formatter): - def formatException(self, ei) -> str: # noqa: ARG002, N802 - return "" + def formatException(self, ei) -> str: # noqa: N802 + (etype, value, tb) = ei + sh = ipylab.app._ipy_shell # noqa: SLF001 + if not sh: + return super().formatException(ei) + itb = sh.InteractiveTB + itb.verbose if ipylab.app.logging_handler.level <= log_name_mappings[LogLevel.debug] else itb.minimal + stb = itb.structured_traceback(etype, value, tb) # type: ignore + return itb.stb2text(stb)