Skip to content

Commit

Permalink
Added logging.
Browse files Browse the repository at this point in the history
  • Loading branch information
Alan Fleming committed Oct 29, 2024
1 parent 118f448 commit ad0ea0a
Show file tree
Hide file tree
Showing 13 changed files with 239 additions and 70 deletions.
35 changes: 0 additions & 35 deletions .github/workflows/lint.yml

This file was deleted.

16 changes: 9 additions & 7 deletions .github/workflows/packaging.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,11 @@ name: Packaging

on:
push:
branches: [ main ]
branches:
- main
pull_request:
branches: '*'
branches:
- '*'

env:
PIP_DISABLE_PIP_VERSION_CHECK: 1
Expand All @@ -23,11 +25,11 @@ jobs:
uses: jupyterlab/maintainer-tools/.github/actions/base-setup@v1
- name: Install dependencies
run: |
pip install .[dev]
- name: Build pypi distributions
run: |
hatch build
- name: Build npm distributions
python -m pip install "jupyterlab>=4,<5" hatch
- name: Build pypi distributions
run: |
hatch build
- name: Build npm distributions
run: |
npm pack
cp *.tgz dist
Expand Down
21 changes: 12 additions & 9 deletions examples/menu.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,10 @@
"metadata": {},
"outputs": [],
"source": [
"import ipylab\n",
"import ipywidgets as ipw\n",
"\n",
"import ipylab\n",
"\n",
"app = ipylab.App()"
]
},
Expand All @@ -54,7 +56,7 @@
"outputs": [],
"source": [
"menu = t.result()\n",
"app.main_menu.add_menu(menu, rank=1000)"
"app.main_menu.add_menu(menu)"
]
},
{
Expand All @@ -78,12 +80,12 @@
"outputs": [],
"source": [
"async def populate_menu(menu):\n",
" menu.add_item(command=\"help:about\")\n",
" menu.add_item(type=\"separator\")\n",
" await menu.add_item(command=\"help:about\")\n",
" await menu.add_item(type=\"separator\")\n",
" submenu = await app.main_menu.create_menu(\"My submenu\")\n",
" submenu.add_item(command=\"notebook:create-console\")\n",
" menu.add_item(submenu=submenu, type=\"submenu\")\n",
" menu.add_item(command=\"logconsole:open\")\n",
" await submenu.add_item(command=\"notebook:create-console\")\n",
" await menu.add_item(submenu=submenu, type=\"submenu\")\n",
" await menu.add_item(command=\"logconsole:open\")\n",
"\n",
" # Open it\n",
" await app.main_menu.set_property(\"activeMenu\", ipylab.pack(menu), toObject=[\"value\"])\n",
Expand Down Expand Up @@ -132,7 +134,7 @@
"metadata": {},
"outputs": [],
"source": [
"panel = ipylab.Panel([ipw.HTML('<h3>Right click to open the context menu')])"
"panel = ipylab.Panel([ipw.HTML(\"<h3>Right click to open the context menu\")])"
]
},
{
Expand Down Expand Up @@ -171,6 +173,7 @@
" await populate_menu(menu)\n",
" await populate_menu(app.context_menu)\n",
"\n",
"\n",
"app.on_ready(create_menus)"
]
},
Expand All @@ -180,7 +183,7 @@
"source": [
"Reload the page (F5) ignoring any warnings.\n",
"\n",
"The panel that was in the shell and the menus should have been restored. \n",
"The panel that was in the shell and the menus should have been restored.\n",
"\n",
"Note: May require a per-kernel widget manager. See Readme for details on installation."
]
Expand Down
6 changes: 3 additions & 3 deletions ipylab/_compat/enum.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,11 @@
import sys

if sys.version_info < (3, 11):
from backports.strenum import StrEnum
from backports.strenum import IntEnum, StrEnum
else:
from enum import StrEnum
from enum import IntEnum, StrEnum

__all__ = ["StrEnum"]
__all__ = ["StrEnum", "IntEnum"]


def __dir__() -> list[str]:
Expand Down
3 changes: 3 additions & 0 deletions ipylab/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
hookimpl = pluggy.HookimplMarker("ipylab") # Used for plugins

if TYPE_CHECKING:
from collections.abc import Awaitable, Callable
from typing import TypeVar, overload

from traitlets import HasTraits
Expand Down Expand Up @@ -236,5 +237,7 @@ class TaskHooks(TypedDict):
add_to_tuple_fwd: NotRequired[list[tuple[HasTraits, str]]]
add_to_tuple_rev: NotRequired[list[tuple[str, Ipylab]]]

callbacks: NotRequired[list[Callable[[Any], None | Awaitable[None]]]]


TaskHookType = TaskHooks | None
6 changes: 6 additions & 0 deletions ipylab/hookspecs.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
hookspec = pluggy.HookspecMarker("ipylab")

if TYPE_CHECKING:
import logging
from collections.abc import Awaitable, Callable

import ipylab
Expand Down Expand Up @@ -71,6 +72,11 @@ def namespace_objects(objects: dict, namespace_name: str, app: ipylab.App) -> No
You can use this to customise the objects available in the namespace."""


@hookspec(firstresult=True)
def get_logger(obj: App) -> logging.Logger | logging.LoggerAdapter: # type: ignore
"Get the logger for the obj."


@hookspec(firstresult=True)
def on_error(obj: ipylab.Ipylab, source: ErrorSource, error: Exception):
"""
Expand Down
15 changes: 11 additions & 4 deletions ipylab/ipylab.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,19 +5,20 @@
import asyncio
import contextlib
import json
import logging
import traceback
import uuid
import weakref
from typing import TYPE_CHECKING, Any, TypeVar, Unpack

from ipywidgets import Widget, register
from traitlets import Bool, Container, Dict, HasTraits, Instance, Set, TraitError, TraitType, Unicode, observe
from traitlets import Bool, Container, Dict, HasTraits, Instance, Set, TraitError, TraitType, Unicode, default, observe

import ipylab._frontend as _fe
from ipylab.common import ErrorSource, IpylabKwgs, Obj, TaskHookType, Transform, TransformType, pack
from ipylab.log import LogPayloadType, LogTypes

if TYPE_CHECKING:
import logging
from asyncio import Task
from collections.abc import Awaitable, Callable, Hashable
from typing import ClassVar
Expand Down Expand Up @@ -96,8 +97,7 @@ class Ipylab(WidgetBase):
_tasks: Container[set[asyncio.Task]] = Set()
_has_attrs_mappings: Container[set[tuple[HasTraits, str]]] = Set()
close_extras: Container[weakref.WeakSet[Widget]] = Instance(weakref.WeakSet, (), help="extra items to close") # type: ignore
if TYPE_CHECKING:
log: logging.Logger
log = Instance(logging.Logger)

@classmethod
def _load_plugin_hooks(cls):
Expand Down Expand Up @@ -131,6 +131,10 @@ def repr_info(self) -> dict[str, Any] | str:
"Extra info to provide for __repr__."
return {}

@default("log")
def _log_default(self):
return self.hook.get_logger(obj=self)

def __new__(cls, **kwgs) -> Self:
model_id = kwgs.get("model_id") or cls._single_map.get(cls._single_key(kwgs)) if cls.SINGLE else None
if model_id and model_id in cls._single_models:
Expand Down Expand Up @@ -354,6 +358,9 @@ def send(self, content, buffers=None):
self.on_error(ErrorSource.SendError, e)
raise e from None

def send_log_message(self, log: LogPayloadType):
self.send({"log": LogTypes.parse(log)})

def to_task(self, aw: Awaitable[T], name: str | None = None, *, hooks: TaskHookType = None) -> Task[T]:
"""Run aw in a task.
Expand Down
9 changes: 7 additions & 2 deletions ipylab/jupyterfrontend.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
import ipywidgets
from IPython.core.getipython import get_ipython
from ipywidgets import TypedTuple, Widget, register
from traitlets import Bool, Container, Dict, Instance, Tuple, Unicode, observe
from traitlets import Bool, Container, Dict, Instance, Tuple, Unicode, UseEnum, observe

import ipylab
import ipylab.hookspecs
Expand All @@ -21,6 +21,7 @@
from ipylab.dialog import Dialog
from ipylab.ipylab import IpylabBase
from ipylab.launcher import Launcher
from ipylab.log import LogLevel
from ipylab.menu import ContextMenu, MainMenu
from ipylab.notification import NotificationManager
from ipylab.sessions import SessionManager
Expand Down Expand Up @@ -51,6 +52,7 @@ class App(Ipylab):
ipylab_base = IpylabBase(Obj.IpylabModel, "app").tag(sync=True)
version = Unicode(read_only=True).tag(sync=True)
current_widget_id = Unicode(read_only=True).tag(sync=True)
logger_level = UseEnum(LogLevel, read_only=True, default_value=LogLevel.warning).tag(sync=True)
current_session = Dict(read_only=True).tag(sync=True)
all_sessions = Tuple(read_only=True).tag(sync=True)
vpath = Unicode(read_only=True).tag(sync=True)
Expand Down Expand Up @@ -84,7 +86,6 @@ def __init_subclass__(cls, **kwargs) -> None:
def __init__(self, **kwgs):
if self._async_widget_base_init_complete:
return

if vpath := kwgs.pop("vpath", None):
self.set_trait("vpath", vpath)
self.set_trait("is_ipylab_kernel", vpath == "ipylab")
Expand Down Expand Up @@ -210,6 +211,10 @@ def open_console(
coro = self._open_console(args, objects=objects or {}, namespace_name=namespace_name, **kwgs)
return self.to_task(coro, "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)
Expand Down
38 changes: 32 additions & 6 deletions ipylab/lib.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,19 @@
from __future__ import annotations

import inspect
import traceback
import logging
from asyncio import Task
from typing import TYPE_CHECKING, Any

from ipywidgets import Widget

import ipylab
from ipylab.common import ErrorSource, IpylabKwgs, TaskHooks, hookimpl
from ipylab.ipylab import Ipylab
from ipylab.log import IpylabFormatter, IpylabLogHandler
from ipylab.notification import NotifyAction

objects = {}

if TYPE_CHECKING:
from collections.abc import Awaitable, Callable
Expand All @@ -33,12 +38,30 @@ def start_app(vpath: str):
ipylab.App(vpath=vpath)


@hookimpl
def get_logger(obj: App):
if "logger" not in objects:
# Use a common logger and setup the handler here too.
objects["logger"] = logging.getLogger("ipylab")
handler = IpylabLogHandler(obj.app)
fmt = "{name}:{message}"
handler.setFormatter(IpylabFormatter(fmt, style="{"))
objects["logger"].addHandler(handler)
return objects["logger"]


@hookimpl
def on_error(obj: Ipylab, source: ErrorSource, error: Exception):
# TODO: Better error logging. Probably using the log.
msg = f"{error!r}"
obj.log.error(msg)
obj.app.dialog.show_error_message(str(source), str(traceback.format_tb(error.__traceback__)))
msg = f"{source} {error}"
obj.log.exception(msg, extra={"source": source, "error": 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=obj.app.toggle_log_console, keep_open=True)
objects["error_task"] = obj.app.notification.notify(msg, type=ipylab.NotificationType.error, actions=[a])


@hookimpl
Expand All @@ -59,7 +82,7 @@ def _ensure_run(obj: ipylab.Ipylab, aw: Callable | Awaitable | None):


@hookimpl
def task_result(obj: Ipylab, result: HasTraits, hooks: TaskHooks): # noqa: ARG001
def task_result(obj: Ipylab, result: HasTraits, hooks: TaskHooks):
# close with
for owner in hooks.pop("close_with_fwd", ()):
# Close result with each item.
Expand Down Expand Up @@ -101,6 +124,9 @@ def task_result(obj: Ipylab, result: HasTraits, hooks: TaskHooks): # noqa: ARG0
else:
owner.set_trait(name, result)

for cb in hooks.pop("callbacks", ()):
_ensure_run(obj, cb(result))

if hooks:
msg = f"Invalid hooks detected: {hooks}"
raise ValueError(msg)
Expand Down
Loading

0 comments on commit ad0ea0a

Please sign in to comment.