euporie 2.3.2__py3-none-any.whl → 2.4.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- euporie/console/__main__.py +3 -1
- euporie/console/app.py +6 -4
- euporie/console/tabs/console.py +34 -9
- euporie/core/__init__.py +6 -1
- euporie/core/__main__.py +1 -1
- euporie/core/app.py +79 -109
- euporie/core/border.py +44 -14
- euporie/core/comm/base.py +5 -4
- euporie/core/comm/ipywidgets.py +11 -11
- euporie/core/comm/registry.py +12 -6
- euporie/core/commands.py +30 -23
- euporie/core/completion.py +1 -4
- euporie/core/config.py +15 -5
- euporie/core/convert/{base.py → core.py} +117 -53
- euporie/core/convert/formats/ansi.py +46 -25
- euporie/core/convert/formats/base64.py +3 -3
- euporie/core/convert/formats/common.py +38 -13
- euporie/core/convert/formats/formatted_text.py +54 -12
- euporie/core/convert/formats/html.py +5 -5
- euporie/core/convert/formats/jpeg.py +1 -1
- euporie/core/convert/formats/markdown.py +4 -4
- euporie/core/convert/formats/pdf.py +1 -1
- euporie/core/convert/formats/pil.py +5 -3
- euporie/core/convert/formats/png.py +7 -6
- euporie/core/convert/formats/rich.py +4 -3
- euporie/core/convert/formats/sixel.py +5 -5
- euporie/core/convert/utils.py +1 -1
- euporie/core/current.py +11 -5
- euporie/core/formatted_text/ansi.py +4 -8
- euporie/core/formatted_text/html.py +1630 -856
- euporie/core/formatted_text/markdown.py +177 -166
- euporie/core/formatted_text/table.py +20 -14
- euporie/core/formatted_text/utils.py +21 -10
- euporie/core/io.py +14 -14
- euporie/core/kernel.py +48 -37
- euporie/core/key_binding/bindings/micro.py +5 -1
- euporie/core/key_binding/bindings/mouse.py +2 -2
- euporie/core/keys.py +3 -0
- euporie/core/launch.py +5 -2
- euporie/core/lexers.py +13 -2
- euporie/core/log.py +135 -139
- euporie/core/margins.py +32 -14
- euporie/core/path.py +273 -0
- euporie/core/processors.py +35 -0
- euporie/core/renderer.py +21 -5
- euporie/core/style.py +34 -19
- euporie/core/tabs/base.py +101 -17
- euporie/core/tabs/notebook.py +72 -30
- euporie/core/terminal.py +56 -48
- euporie/core/utils.py +12 -16
- euporie/core/widgets/cell.py +6 -5
- euporie/core/widgets/cell_outputs.py +2 -2
- euporie/core/widgets/decor.py +74 -82
- euporie/core/widgets/dialog.py +132 -28
- euporie/core/widgets/display.py +76 -24
- euporie/core/widgets/file_browser.py +87 -31
- euporie/core/widgets/formatted_text_area.py +1 -3
- euporie/core/widgets/forms.py +79 -40
- euporie/core/widgets/inputs.py +23 -13
- euporie/core/widgets/layout.py +4 -3
- euporie/core/widgets/menu.py +368 -216
- euporie/core/widgets/page.py +99 -58
- euporie/core/widgets/pager.py +1 -1
- euporie/core/widgets/palette.py +30 -27
- euporie/core/widgets/search_bar.py +38 -25
- euporie/core/widgets/status_bar.py +103 -5
- euporie/data/desktop/euporie-console.desktop +7 -0
- euporie/data/desktop/euporie-notebook.desktop +7 -0
- euporie/hub/__main__.py +3 -1
- euporie/hub/app.py +9 -7
- euporie/notebook/__main__.py +3 -1
- euporie/notebook/app.py +7 -30
- euporie/notebook/tabs/__init__.py +7 -3
- euporie/notebook/tabs/display.py +18 -9
- euporie/notebook/tabs/edit.py +106 -23
- euporie/notebook/tabs/json.py +73 -0
- euporie/notebook/tabs/log.py +18 -8
- euporie/notebook/tabs/notebook.py +60 -41
- euporie/preview/__main__.py +3 -1
- euporie/preview/app.py +2 -1
- euporie/preview/tabs/notebook.py +23 -10
- euporie/web/tabs/web.py +149 -0
- euporie/web/widgets/webview.py +563 -0
- euporie-2.4.1.data/data/share/applications/euporie-console.desktop +7 -0
- euporie-2.4.1.data/data/share/applications/euporie-notebook.desktop +7 -0
- {euporie-2.3.2.dist-info → euporie-2.4.1.dist-info}/METADATA +6 -5
- euporie-2.4.1.dist-info/RECORD +129 -0
- {euporie-2.3.2.dist-info → euporie-2.4.1.dist-info}/WHEEL +1 -1
- euporie/core/url.py +0 -64
- euporie-2.3.2.dist-info/RECORD +0 -122
- {euporie-2.3.2.dist-info → euporie-2.4.1.dist-info}/entry_points.txt +0 -0
- {euporie-2.3.2.dist-info → euporie-2.4.1.dist-info}/licenses/LICENSE +0 -0
euporie/core/kernel.py
CHANGED
@@ -9,16 +9,17 @@ import threading
|
|
9
9
|
from collections import defaultdict
|
10
10
|
from subprocess import DEVNULL # noqa S404 - Security implications considered
|
11
11
|
from typing import TYPE_CHECKING, TypedDict
|
12
|
+
from uuid import uuid4
|
12
13
|
|
13
14
|
import nbformat
|
14
15
|
from _frozen_importlib import _DeadlockError
|
15
16
|
from jupyter_client import AsyncKernelManager, KernelManager
|
16
17
|
from jupyter_client.kernelspec import NATIVE_KERNEL_NAME, NoSuchKernel
|
17
|
-
from jupyter_core.paths import jupyter_path
|
18
|
-
|
19
|
-
from euporie.core.config import add_setting
|
18
|
+
from jupyter_core.paths import jupyter_path, jupyter_runtime_dir
|
19
|
+
from upath import UPath
|
20
20
|
|
21
21
|
if TYPE_CHECKING:
|
22
|
+
from pathlib import Path
|
22
23
|
from typing import Any, Callable, Coroutine
|
23
24
|
|
24
25
|
from jupyter_client import KernelClient
|
@@ -50,20 +51,13 @@ class Kernel:
|
|
50
51
|
Has the ability to run itself in it's own thread.
|
51
52
|
"""
|
52
53
|
|
53
|
-
def _setup_loop(self) -> None:
|
54
|
-
"""Set the current loop the the kernel's event loop.
|
55
|
-
|
56
|
-
This method is intended to be run in the kernel thread.
|
57
|
-
"""
|
58
|
-
asyncio.set_event_loop(self.loop)
|
59
|
-
self.loop.run_forever()
|
60
|
-
|
61
54
|
def __init__(
|
62
55
|
self,
|
63
56
|
kernel_tab: KernelTab,
|
64
57
|
threaded: bool = True,
|
65
58
|
allow_stdin: bool = False,
|
66
59
|
default_callbacks: MsgCallbacks | None = None,
|
60
|
+
connection_file: Path | None = None,
|
67
61
|
) -> None:
|
68
62
|
"""Call when the :py:class:`Kernel` is initialized.
|
69
63
|
|
@@ -72,6 +66,8 @@ class Kernel:
|
|
72
66
|
threaded: If :py:const:`True`, run kernel communication in a separate thread
|
73
67
|
allow_stdin: Whether the kernel is allowed to request input
|
74
68
|
default_callbacks: The default callbacks to use on recipt of a message
|
69
|
+
connection_file: Path to a file from which to load or to hwich to save
|
70
|
+
kernel connection information
|
75
71
|
|
76
72
|
"""
|
77
73
|
self.threaded = threaded
|
@@ -86,6 +82,7 @@ class Kernel:
|
|
86
82
|
self.allow_stdin = allow_stdin
|
87
83
|
|
88
84
|
self.kernel_tab = kernel_tab
|
85
|
+
self.connection_file = connection_file
|
89
86
|
self.kc: KernelClient | None = None
|
90
87
|
self.km = AsyncKernelManager(
|
91
88
|
kernel_name=str(kernel_tab.kernel_name),
|
@@ -125,7 +122,14 @@ class Kernel:
|
|
125
122
|
# Also this speeds up launch since importing IPython is pretty slow.
|
126
123
|
self.km.kernel_spec_manager.kernel_dirs = jupyter_path("kernels")
|
127
124
|
|
125
|
+
def _setup_loop(self) -> None:
|
126
|
+
"""Set the current loop the the kernel's event loop.
|
127
|
+
|
128
|
+
This method is intended to be run in the kernel thread.
|
129
|
+
"""
|
130
|
+
asyncio.set_event_loop(self.loop)
|
128
131
|
self.status_change_event = asyncio.Event()
|
132
|
+
self.loop.run_forever()
|
129
133
|
|
130
134
|
def _aodo(
|
131
135
|
self,
|
@@ -234,7 +238,7 @@ class Kernel:
|
|
234
238
|
def missing(self) -> bool:
|
235
239
|
"""Return True if the requested kernel is not found."""
|
236
240
|
try:
|
237
|
-
self.km.kernel_spec
|
241
|
+
self.km.kernel_spec # noqa B018
|
238
242
|
except NoSuchKernel:
|
239
243
|
return True
|
240
244
|
else:
|
@@ -272,16 +276,27 @@ class Kernel:
|
|
272
276
|
|
273
277
|
# If we are connecting to an existing kernel, create a kernel client using
|
274
278
|
# the given connection file
|
275
|
-
|
276
|
-
|
277
|
-
|
278
|
-
|
279
|
+
runtime_dir = UPath(jupyter_runtime_dir())
|
280
|
+
if (connection_file := self.connection_file) is None:
|
281
|
+
id_ = str(uuid4())[:8]
|
282
|
+
connection_file = runtime_dir / f"kernel-euporie-{id_}.json"
|
283
|
+
connection_file_str = str(connection_file)
|
284
|
+
self.km.connection_file = connection_file_str
|
285
|
+
|
286
|
+
if connection_file.exists():
|
287
|
+
log.debug(
|
288
|
+
"Connecting to existing kernel using connection file '%s'",
|
289
|
+
connection_file,
|
290
|
+
)
|
291
|
+
self.km.load_connection_file(connection_file_str)
|
292
|
+
kc = self.km.client_factory(connection_file=connection_file_str)
|
279
293
|
kc.load_connection_file()
|
280
294
|
kc.start_channels()
|
281
295
|
self.kc = kc
|
282
296
|
|
283
297
|
# Otherwise, start a new kernel using the kernel manager
|
284
298
|
else:
|
299
|
+
runtime_dir.mkdir(exist_ok=True, parents=True)
|
285
300
|
while True:
|
286
301
|
try:
|
287
302
|
# TODO - send stdout to log
|
@@ -997,22 +1012,29 @@ class Kernel:
|
|
997
1012
|
callback=cb,
|
998
1013
|
)
|
999
1014
|
|
1000
|
-
def change(
|
1015
|
+
def change(
|
1016
|
+
self,
|
1017
|
+
name: str | None,
|
1018
|
+
connection_file: Path | None = None,
|
1019
|
+
cb: Callable | None = None,
|
1020
|
+
) -> None:
|
1001
1021
|
"""Change the kernel.
|
1002
1022
|
|
1003
1023
|
Args:
|
1004
1024
|
name: The name of the kernel to change to
|
1025
|
+
connection_file: The path to the connection file to use
|
1005
1026
|
cb: Callback to run once restarted
|
1006
1027
|
|
1007
1028
|
"""
|
1029
|
+
self.connection_file = connection_file
|
1008
1030
|
self.status = "starting"
|
1009
1031
|
|
1010
1032
|
# Update the tab's kernel spec
|
1011
|
-
spec = self.specs.get(name, {}).get("spec", {})
|
1033
|
+
spec = self.specs.get(name or "", {}).get("spec", {})
|
1012
1034
|
self.kernel_tab.metadata["kernelspec"] = {
|
1013
1035
|
"name": name,
|
1014
|
-
"display_name": spec
|
1015
|
-
"language": spec
|
1036
|
+
"display_name": spec.get("display_name", ""),
|
1037
|
+
"language": spec.get("language", ""),
|
1016
1038
|
}
|
1017
1039
|
|
1018
1040
|
# Stop the old kernel
|
@@ -1021,7 +1043,8 @@ class Kernel:
|
|
1021
1043
|
|
1022
1044
|
# Create a new kernel manager instance
|
1023
1045
|
del self.km
|
1024
|
-
|
1046
|
+
kwargs = {} if name is None else {"kernel_name": name}
|
1047
|
+
self.km = AsyncKernelManager(**kwargs)
|
1025
1048
|
self.error = None
|
1026
1049
|
|
1027
1050
|
# Start the kernel
|
@@ -1053,12 +1076,14 @@ class Kernel:
|
|
1053
1076
|
|
1054
1077
|
async def shutdown_(self) -> None:
|
1055
1078
|
"""Shut down the kernel and close the event loop if running in a thread."""
|
1079
|
+
# Clean up connection file
|
1080
|
+
self.km.cleanup_connection_file()
|
1081
|
+
# Stop kernel
|
1056
1082
|
if self.km.has_kernel:
|
1057
1083
|
await self.km.shutdown_kernel(now=True)
|
1084
|
+
# Stop event loop
|
1058
1085
|
if self.threaded:
|
1059
1086
|
self.loop.stop()
|
1060
|
-
self.loop.close()
|
1061
|
-
log.debug("Loop closed")
|
1062
1087
|
|
1063
1088
|
def shutdown(self, wait: bool = False) -> None:
|
1064
1089
|
"""Shutdown the kernel and close the kernel's thread.
|
@@ -1076,17 +1101,3 @@ class Kernel:
|
|
1076
1101
|
)
|
1077
1102
|
if self.threaded:
|
1078
1103
|
self.thread.join(timeout=5)
|
1079
|
-
|
1080
|
-
# ################################### Settings ####################################
|
1081
|
-
|
1082
|
-
add_setting(
|
1083
|
-
name="kernel_connection_file",
|
1084
|
-
flags=["--kernel-connection-file"],
|
1085
|
-
type_=str,
|
1086
|
-
help_="Attempt to connect to an existing kernel using a JSON connection info file",
|
1087
|
-
default="",
|
1088
|
-
description="""
|
1089
|
-
Load connection info from JSON dict. This allows euporie to connect to
|
1090
|
-
existing kernels.
|
1091
|
-
""",
|
1092
|
-
)
|
@@ -93,7 +93,11 @@ register_bindings(
|
|
93
93
|
"newline": "enter",
|
94
94
|
"accept-line": "enter",
|
95
95
|
"backspace": ["backspace", "c-h"],
|
96
|
-
"backward-kill-word": [
|
96
|
+
"backward-kill-word": [
|
97
|
+
"c-backspace",
|
98
|
+
("escape", "backspace"),
|
99
|
+
("escape", "c-h"),
|
100
|
+
],
|
97
101
|
"start-selection": [
|
98
102
|
"s-up",
|
99
103
|
"s-down",
|
@@ -154,11 +154,11 @@ def load_mouse_bindings() -> "KeyBindings":
|
|
154
154
|
if (mouse_limits := event.app.mouse_limits) is not None:
|
155
155
|
x = max(
|
156
156
|
mouse_limits.xpos,
|
157
|
-
min(x, mouse_limits.xpos + mouse_limits.width - 1),
|
157
|
+
min(x, mouse_limits.xpos + (mouse_limits.width - 1)),
|
158
158
|
)
|
159
159
|
y = max(
|
160
160
|
mouse_limits.ypos,
|
161
|
-
min(y, mouse_limits.ypos + mouse_limits.height - 1),
|
161
|
+
min(y, mouse_limits.ypos + (mouse_limits.height - 1)),
|
162
162
|
)
|
163
163
|
|
164
164
|
# Call the mouse handler from the renderer.
|
euporie/core/keys.py
CHANGED
@@ -8,6 +8,7 @@ from prompt_toolkit.keys import Keys
|
|
8
8
|
extend_enum(Keys, "ControlEnter", "c-enter")
|
9
9
|
extend_enum(Keys, "ControlShiftEnter", "c-s-enter")
|
10
10
|
extend_enum(Keys, "ShiftEnter", "s-enter")
|
11
|
+
extend_enum(Keys, "ControlBackspace", "c-backspace")
|
11
12
|
|
12
13
|
# Assign escape sequences to new keys
|
13
14
|
ANSI_SEQUENCES["\x1b[27;5;13~"] = Keys.ControlEnter # type: ignore
|
@@ -20,6 +21,7 @@ ANSI_SEQUENCES["\x1b[27;6;13~"] = Keys.ControlShiftEnter # type: ignore
|
|
20
21
|
ANSI_SEQUENCES["\x1b[13;6u"] = Keys.ControlShiftEnter # type: ignore
|
21
22
|
|
22
23
|
# CSI-u control+key
|
24
|
+
ANSI_SEQUENCES["\x1b[32;5u"] = Keys.ControlSpace # type: ignore
|
23
25
|
ANSI_SEQUENCES["\x1b[97;5u"] = Keys.ControlA # type: ignore
|
24
26
|
ANSI_SEQUENCES["\x1b[98;5u"] = Keys.ControlB # type: ignore
|
25
27
|
ANSI_SEQUENCES["\x1b[99;5u"] = Keys.ControlC # type: ignore
|
@@ -46,5 +48,6 @@ ANSI_SEQUENCES["\x1b[119;5u"] = Keys.ControlW # type: ignore
|
|
46
48
|
ANSI_SEQUENCES["\x1b[120;5u"] = Keys.ControlX # type: ignore
|
47
49
|
ANSI_SEQUENCES["\x1b[121;5u"] = Keys.ControlY # type: ignore
|
48
50
|
ANSI_SEQUENCES["\x1b[122;5u"] = Keys.ControlZ # type: ignore
|
51
|
+
ANSI_SEQUENCES["\x1b[127;5u"] = Keys.ControlBackspace # type: ignore
|
49
52
|
ANSI_SEQUENCES["\x1b[27;2;9~"] = Keys.BackTab # type: ignore
|
50
53
|
ANSI_SEQUENCES["\x1b[9;2u"] = Keys.BackTab # type: ignore
|
euporie/core/launch.py
CHANGED
@@ -5,7 +5,7 @@ from __future__ import annotations
|
|
5
5
|
from importlib.metadata import entry_points
|
6
6
|
|
7
7
|
from euporie.core.config import Config, add_setting
|
8
|
-
from euporie.core.log import
|
8
|
+
from euporie.core.log import setup_logs
|
9
9
|
|
10
10
|
APP_ALIASES = {
|
11
11
|
"edit": "notebook",
|
@@ -23,12 +23,15 @@ class CoreApp:
|
|
23
23
|
def launch(cls) -> None:
|
24
24
|
"""Launch the app."""
|
25
25
|
# Set up default logging
|
26
|
-
|
26
|
+
setup_logs()
|
27
27
|
|
28
28
|
# Load the launcher's configuration
|
29
29
|
cls.config.load(cls)
|
30
30
|
app = cls.config.app
|
31
31
|
|
32
|
+
# Remove the app setting from the list of known settings
|
33
|
+
del Config.settings["app"]
|
34
|
+
|
32
35
|
# Add aliases
|
33
36
|
app = APP_ALIASES.get(app, app)
|
34
37
|
|
euporie/core/lexers.py
CHANGED
@@ -1,8 +1,11 @@
|
|
1
1
|
"""Relating to lexers."""
|
2
2
|
|
3
|
+
from __future__ import annotations
|
4
|
+
|
3
5
|
from typing import TYPE_CHECKING
|
4
6
|
|
5
7
|
from pygments.lexers import (
|
8
|
+
get_lexer_by_name,
|
6
9
|
get_lexer_for_filename,
|
7
10
|
guess_lexer,
|
8
11
|
guess_lexer_for_filename,
|
@@ -10,11 +13,14 @@ from pygments.lexers import (
|
|
10
13
|
from pygments.util import ClassNotFound
|
11
14
|
|
12
15
|
if TYPE_CHECKING:
|
16
|
+
from pathlib import Path
|
17
|
+
|
13
18
|
from pygments.lexer import Lexer as PygmentsLexerCls
|
14
|
-
from upath import UPath
|
15
19
|
|
16
20
|
|
17
|
-
def detect_lexer(
|
21
|
+
def detect_lexer(
|
22
|
+
text: str = "", path: Path | None = None, language: str = ""
|
23
|
+
) -> PygmentsLexerCls | None:
|
18
24
|
"""Detect the pygments lexer for a file."""
|
19
25
|
lexer = None
|
20
26
|
if path is not None:
|
@@ -25,6 +31,11 @@ def detect_lexer(text: "str" = "", path: "UPath|None" = None) -> "PygmentsLexerC
|
|
25
31
|
lexer = guess_lexer_for_filename(path, text)
|
26
32
|
except ClassNotFound:
|
27
33
|
pass
|
34
|
+
if lexer is None and language:
|
35
|
+
try:
|
36
|
+
lexer = get_lexer_by_name(language)
|
37
|
+
except ClassNotFound:
|
38
|
+
pass
|
28
39
|
if lexer is None:
|
29
40
|
try:
|
30
41
|
lexer = guess_lexer(text)
|
euporie/core/log.py
CHANGED
@@ -22,8 +22,10 @@ from prompt_toolkit.styles.pygments import style_from_pygments_cls
|
|
22
22
|
from prompt_toolkit.styles.style import Style, merge_styles
|
23
23
|
from pygments.styles import get_style_by_name
|
24
24
|
|
25
|
+
from euporie.core.config import add_setting
|
25
26
|
from euporie.core.formatted_text.utils import indent, lex, wrap
|
26
27
|
from euporie.core.style import LOG_STYLE
|
28
|
+
from euporie.core.utils import dict_merge
|
27
29
|
|
28
30
|
if TYPE_CHECKING:
|
29
31
|
from types import TracebackType
|
@@ -39,22 +41,8 @@ log = logging.getLogger(__name__)
|
|
39
41
|
LOG_QUEUE: deque = deque(maxlen=1000)
|
40
42
|
|
41
43
|
|
42
|
-
def dict_merge(target_dict: dict, input_dict: dict) -> None:
|
43
|
-
"""Merge the second dictionary onto the first."""
|
44
|
-
for k in input_dict:
|
45
|
-
if k in target_dict:
|
46
|
-
if isinstance(target_dict[k], dict) and isinstance(input_dict[k], dict):
|
47
|
-
dict_merge(target_dict[k], input_dict[k])
|
48
|
-
elif isinstance(target_dict[k], list) and isinstance(input_dict[k], list):
|
49
|
-
target_dict[k] = [*target_dict[k], *input_dict[k]]
|
50
|
-
else:
|
51
|
-
target_dict[k] = input_dict[k]
|
52
|
-
else:
|
53
|
-
target_dict[k] = input_dict[k]
|
54
|
-
|
55
|
-
|
56
44
|
class FtFormatter(logging.Formatter):
|
57
|
-
"""
|
45
|
+
"""Base class for formatted text logging formatter."""
|
58
46
|
|
59
47
|
def __init__(self, *args: Any, **kwargs: Any) -> None:
|
60
48
|
"""Create a new formatter instance."""
|
@@ -201,16 +189,14 @@ class LogTabFormatter(FtFormatter):
|
|
201
189
|
"""Format a log record as formatted text."""
|
202
190
|
record = self.prepare(record)
|
203
191
|
output: StyleAndTextTuples = [
|
192
|
+
("", "["),
|
204
193
|
("class:pygments.literal.date", f"{record.asctime}"),
|
205
|
-
("", " "),
|
194
|
+
("", "] ["),
|
206
195
|
(f"class:log.level.{record.levelname}", f"{record.levelname}"),
|
207
|
-
("", " "
|
196
|
+
("", "] ["),
|
197
|
+
("class:pygments.comment", f"{record.name}"),
|
198
|
+
("", "] "),
|
208
199
|
("class:log,msg", record.message),
|
209
|
-
("", " "),
|
210
|
-
(
|
211
|
-
"class:pygments.comment",
|
212
|
-
f"{record.name}.{record.funcName}:{record.lineno}",
|
213
|
-
),
|
214
200
|
("", "\n"),
|
215
201
|
]
|
216
202
|
if record.exc_text:
|
@@ -283,82 +269,6 @@ class StdoutFormatter(FtFormatter):
|
|
283
269
|
return FormattedText(output)
|
284
270
|
|
285
271
|
|
286
|
-
def setup_logs(config: Config) -> None:
|
287
|
-
"""Configure the logger for euporie."""
|
288
|
-
log_file_is_stdout = config.log_file in ("-", "/dev/stdout")
|
289
|
-
|
290
|
-
log_config = {
|
291
|
-
"version": 1,
|
292
|
-
"disable_existing_loggers": False,
|
293
|
-
"formatters": {
|
294
|
-
"file_format": {
|
295
|
-
"format": "{asctime} {levelname:<7} [{name}.{funcName}:{lineno}] {message}",
|
296
|
-
"style": "{",
|
297
|
-
"datefmt": "%Y-%m-%d %H:%M:%S",
|
298
|
-
},
|
299
|
-
"stdout_format": {
|
300
|
-
"()": "euporie.core.log.StdoutFormatter",
|
301
|
-
},
|
302
|
-
"log_tab_format": {
|
303
|
-
"()": "euporie.core.log.LogTabFormatter",
|
304
|
-
},
|
305
|
-
},
|
306
|
-
"handlers": {
|
307
|
-
**(
|
308
|
-
{
|
309
|
-
"file": {
|
310
|
-
"level": config.log_level.upper() or "ERROR",
|
311
|
-
"class": "logging.FileHandler",
|
312
|
-
"filename": Path(config.log_file).expanduser(),
|
313
|
-
"formatter": "file_format",
|
314
|
-
}
|
315
|
-
}
|
316
|
-
if config.log_file and not log_file_is_stdout
|
317
|
-
else {}
|
318
|
-
),
|
319
|
-
"stdout": {
|
320
|
-
"level": config.log_level.upper()
|
321
|
-
if config.log_level and log_file_is_stdout
|
322
|
-
else (
|
323
|
-
"critical"
|
324
|
-
if (app_cls := config.app_cls) is None
|
325
|
-
else app_cls.log_stdout_level
|
326
|
-
),
|
327
|
-
"class": "euporie.core.log.FormattedTextHandler",
|
328
|
-
"pygments_theme": config.syntax_theme,
|
329
|
-
"formatter": "stdout_format",
|
330
|
-
"stream": sys.stdout,
|
331
|
-
},
|
332
|
-
"log_tab": {
|
333
|
-
"level": config.log_level.upper() or "INFO",
|
334
|
-
"class": "euporie.core.log.QueueHandler",
|
335
|
-
"formatter": "log_tab_format",
|
336
|
-
"queue": LOG_QUEUE,
|
337
|
-
},
|
338
|
-
},
|
339
|
-
"loggers": {
|
340
|
-
"euporie": {
|
341
|
-
"level": config.log_level.upper() or "INFO",
|
342
|
-
"handlers": ["log_tab", "stdout"]
|
343
|
-
+ (["file"] if not log_file_is_stdout and config.log_file else []),
|
344
|
-
"propagate": False,
|
345
|
-
},
|
346
|
-
},
|
347
|
-
# Log everything to the internal logger
|
348
|
-
"root": {"handlers": ["log_tab"]},
|
349
|
-
}
|
350
|
-
# Update log_config based additional config provided
|
351
|
-
if config.log_config:
|
352
|
-
import json
|
353
|
-
|
354
|
-
extra_config = json.loads(config.log_config)
|
355
|
-
dict_merge(log_config, extra_config)
|
356
|
-
# Configure the logger
|
357
|
-
# Pytype used TypedDicts to validate the dictionary structure, but I cannot get
|
358
|
-
# this to work for some reason...
|
359
|
-
logging.config.dictConfig(log_config) # type: ignore
|
360
|
-
|
361
|
-
|
362
272
|
class stdout_to_log:
|
363
273
|
"""A decorator which captures standard output and logs it."""
|
364
274
|
|
@@ -425,53 +335,139 @@ def handle_exception(
|
|
425
335
|
log.critical("Uncaught exception", exc_info=(exc_type, exc_value, exc_traceback))
|
426
336
|
|
427
337
|
|
428
|
-
def
|
429
|
-
"""
|
430
|
-
|
431
|
-
|
432
|
-
|
433
|
-
|
434
|
-
|
435
|
-
|
436
|
-
|
437
|
-
|
438
|
-
|
439
|
-
|
440
|
-
|
441
|
-
|
442
|
-
},
|
443
|
-
"log_tab_format": {
|
444
|
-
"()": LogTabFormatter,
|
445
|
-
},
|
338
|
+
def setup_logs(config: Config | None = None) -> None:
|
339
|
+
"""Configure the logger for euporie."""
|
340
|
+
# Default log config
|
341
|
+
log_config: dict[str, Any] = {
|
342
|
+
"version": 1,
|
343
|
+
"disable_existing_loggers": False,
|
344
|
+
"formatters": {
|
345
|
+
"file_format": {
|
346
|
+
"format": "{asctime} {levelname:<7} [{name}.{funcName}:{lineno}] {message}",
|
347
|
+
"style": "{",
|
348
|
+
"datefmt": "%Y-%m-%d %H:%M:%S",
|
349
|
+
},
|
350
|
+
"stdout_format": {
|
351
|
+
"()": StdoutFormatter,
|
446
352
|
},
|
447
|
-
"
|
448
|
-
"
|
449
|
-
"level": "INFO",
|
450
|
-
"()": FormattedTextHandler,
|
451
|
-
"formatter": "stdout_format",
|
452
|
-
"stream": sys.stdout,
|
453
|
-
},
|
454
|
-
"log_tab": {
|
455
|
-
"level": "INFO",
|
456
|
-
"()": QueueHandler,
|
457
|
-
"formatter": "log_tab_format",
|
458
|
-
"queue": LOG_QUEUE,
|
459
|
-
},
|
353
|
+
"log_tab_format": {
|
354
|
+
"()": LogTabFormatter,
|
460
355
|
},
|
461
|
-
|
462
|
-
|
463
|
-
|
464
|
-
|
465
|
-
|
466
|
-
|
356
|
+
},
|
357
|
+
"handlers": {
|
358
|
+
"stdout": {
|
359
|
+
"level": "INFO",
|
360
|
+
"()": FormattedTextHandler,
|
361
|
+
"formatter": "stdout_format",
|
362
|
+
"stream": sys.stdout,
|
363
|
+
},
|
364
|
+
"log_tab": {
|
365
|
+
"level": "INFO",
|
366
|
+
"()": QueueHandler,
|
367
|
+
"formatter": "log_tab_format",
|
368
|
+
"queue": LOG_QUEUE,
|
369
|
+
},
|
370
|
+
},
|
371
|
+
"loggers": {
|
372
|
+
"euporie": {
|
373
|
+
"level": "INFO",
|
374
|
+
"handlers": ["log_tab", "stdout"],
|
375
|
+
"propagate": False,
|
467
376
|
},
|
468
|
-
|
469
|
-
|
470
|
-
}
|
471
|
-
|
377
|
+
},
|
378
|
+
# Log everything to the internal logger
|
379
|
+
"root": {"handlers": ["log_tab"]},
|
380
|
+
}
|
381
|
+
|
382
|
+
if config is not None:
|
383
|
+
log_file = config.get("log_file", "")
|
384
|
+
log_file_is_stdout = log_file in {"-", "/dev/stdout"}
|
385
|
+
log_level = config.log_level.upper()
|
386
|
+
|
387
|
+
# Configure file handler
|
388
|
+
if log_file and not log_file_is_stdout:
|
389
|
+
log_config["handlers"]["file"] = {
|
390
|
+
"level": log_level,
|
391
|
+
"class": "logging.FileHandler",
|
392
|
+
"filename": Path(config.log_file).expanduser(),
|
393
|
+
"formatter": "file_format",
|
394
|
+
}
|
395
|
+
log_config["loggers"]["euporie"]["handlers"].append("file")
|
396
|
+
|
397
|
+
# Configure stdout handler
|
398
|
+
if log_file_is_stdout:
|
399
|
+
stdout_level = log_level
|
400
|
+
elif (app_cls := config.app_cls) is not None and (
|
401
|
+
log_stdout_level := app_cls.log_stdout_level
|
402
|
+
):
|
403
|
+
stdout_level = log_stdout_level.upper()
|
404
|
+
else:
|
405
|
+
stdout_level = "CRITICAL"
|
406
|
+
log_config["handlers"]["stdout"]["level"] = stdout_level
|
407
|
+
log_config["handlers"]["stdout"]["pygments_theme"] = config.get(
|
408
|
+
"syntax_theme", "euporie"
|
409
|
+
)
|
410
|
+
|
411
|
+
# Configure euporie logger
|
412
|
+
log_config["loggers"]["euporie"]["level"] = config.log_level.upper()
|
413
|
+
|
414
|
+
# Update log_config based on additional config dict provided
|
415
|
+
if config.log_config:
|
416
|
+
import json
|
417
|
+
|
418
|
+
extra_config = json.loads(config.log_config)
|
419
|
+
dict_merge(log_config, extra_config)
|
420
|
+
|
421
|
+
# Configure the logger
|
422
|
+
# Pytype used TypedDicts to validate the dictionary structure, but I cannot get
|
423
|
+
# this to work for some reason...
|
424
|
+
logging.config.dictConfig(log_config) # type: ignore
|
472
425
|
|
473
426
|
# Capture warnings so they show up in the logs
|
474
427
|
logging.captureWarnings(True)
|
475
428
|
|
476
429
|
# Log uncaught exceptions
|
477
430
|
sys.excepthook = handle_exception
|
431
|
+
|
432
|
+
|
433
|
+
# ################################### Settings ########################################
|
434
|
+
|
435
|
+
|
436
|
+
add_setting(
|
437
|
+
name="log_file",
|
438
|
+
flags=["--log-file"],
|
439
|
+
nargs="?",
|
440
|
+
default="",
|
441
|
+
type_=str,
|
442
|
+
title="the log file path",
|
443
|
+
help_="File path for logs",
|
444
|
+
description="""
|
445
|
+
When set to a file path, the log output will be written to the given path.
|
446
|
+
If no value is given output will be sent to the standard output.
|
447
|
+
""",
|
448
|
+
)
|
449
|
+
|
450
|
+
add_setting(
|
451
|
+
name="log_level",
|
452
|
+
flags=["--log-level"],
|
453
|
+
type_=str,
|
454
|
+
default="warning",
|
455
|
+
title="the log level",
|
456
|
+
help_="Set the log level",
|
457
|
+
choices=["debug", "info", "warning", "error", "critical"],
|
458
|
+
description="""
|
459
|
+
When set, logging events at the given level are emitted.
|
460
|
+
""",
|
461
|
+
)
|
462
|
+
|
463
|
+
add_setting(
|
464
|
+
name="log_config",
|
465
|
+
flags=["--log-config"],
|
466
|
+
type_=str,
|
467
|
+
default=None,
|
468
|
+
title="additional logging configuration",
|
469
|
+
help_="Additional logging configuration",
|
470
|
+
description="""
|
471
|
+
A JSON string specifying additional logging configuration.
|
472
|
+
""",
|
473
|
+
)
|