euporie 2.8.4__py3-none-any.whl → 2.8.6__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/_commands.py +143 -0
- euporie/console/_settings.py +58 -0
- euporie/console/app.py +25 -71
- euporie/console/tabs/console.py +58 -62
- euporie/core/__init__.py +1 -1
- euporie/core/__main__.py +28 -11
- euporie/core/_settings.py +109 -0
- euporie/core/app/__init__.py +3 -0
- euporie/core/app/_commands.py +95 -0
- euporie/core/app/_settings.py +457 -0
- euporie/core/{app.py → app/app.py} +212 -576
- euporie/core/app/base.py +51 -0
- euporie/core/{current.py → app/current.py} +13 -4
- euporie/core/app/cursor.py +35 -0
- euporie/core/app/dummy.py +12 -0
- euporie/core/app/launch.py +28 -0
- euporie/core/bars/__init__.py +11 -0
- euporie/core/bars/command.py +205 -0
- euporie/core/bars/menu.py +258 -0
- euporie/core/{widgets → bars}/search.py +20 -16
- euporie/core/{widgets → bars}/status.py +6 -23
- euporie/core/clipboard.py +19 -80
- euporie/core/comm/base.py +8 -6
- euporie/core/comm/ipywidgets.py +16 -7
- euporie/core/comm/registry.py +2 -1
- euporie/core/commands.py +10 -20
- euporie/core/completion.py +3 -2
- euporie/core/config.py +368 -341
- euporie/core/convert/__init__.py +0 -30
- euporie/core/convert/datum.py +116 -53
- euporie/core/convert/formats/__init__.py +31 -0
- euporie/core/convert/formats/ansi.py +9 -23
- euporie/core/convert/formats/common.py +11 -23
- euporie/core/convert/formats/html.py +45 -40
- euporie/core/convert/formats/pil.py +1 -1
- euporie/core/convert/formats/png.py +3 -5
- euporie/core/convert/formats/sixel.py +3 -3
- euporie/core/convert/registry.py +4 -6
- euporie/core/convert/utils.py +41 -4
- euporie/core/diagnostics.py +2 -2
- euporie/core/filters.py +98 -40
- euporie/core/format.py +2 -3
- euporie/core/ft/ansi.py +1 -1
- euporie/core/ft/html.py +12 -21
- euporie/core/ft/table.py +1 -3
- euporie/core/ft/utils.py +4 -1
- euporie/core/graphics.py +386 -133
- euporie/core/history.py +2 -2
- euporie/core/inspection.py +3 -2
- euporie/core/io.py +207 -28
- euporie/core/kernel/__init__.py +1 -0
- euporie/core/{kernel.py → kernel/client.py} +45 -108
- euporie/core/kernel/manager.py +114 -0
- euporie/core/key_binding/bindings/__init__.py +1 -8
- euporie/core/key_binding/bindings/basic.py +47 -7
- euporie/core/key_binding/bindings/completion.py +3 -8
- euporie/core/key_binding/bindings/micro.py +1 -6
- euporie/core/key_binding/bindings/mouse.py +2 -2
- euporie/core/key_binding/bindings/terminal.py +193 -0
- euporie/core/key_binding/key_processor.py +43 -2
- euporie/core/key_binding/registry.py +2 -0
- euporie/core/key_binding/utils.py +22 -2
- euporie/core/keys.py +7156 -93
- euporie/core/layout/cache.py +3 -3
- euporie/core/layout/containers.py +48 -4
- euporie/core/layout/decor.py +2 -2
- euporie/core/layout/mouse.py +1 -1
- euporie/core/layout/print.py +2 -1
- euporie/core/layout/scroll.py +39 -34
- euporie/core/log.py +76 -64
- euporie/core/lsp.py +118 -24
- euporie/core/margins.py +1 -1
- euporie/core/path.py +62 -13
- euporie/core/renderer.py +58 -17
- euporie/core/style.py +57 -39
- euporie/core/suggest.py +103 -85
- euporie/core/tabs/__init__.py +32 -0
- euporie/core/tabs/_settings.py +113 -0
- euporie/core/tabs/base.py +80 -470
- euporie/core/tabs/kernel.py +419 -0
- euporie/core/tabs/notebook.py +24 -101
- euporie/core/utils.py +92 -15
- euporie/core/validation.py +1 -1
- euporie/core/widgets/_settings.py +188 -0
- euporie/core/widgets/cell.py +19 -50
- euporie/core/widgets/cell_outputs.py +25 -36
- euporie/core/widgets/decor.py +11 -41
- euporie/core/widgets/dialog.py +62 -27
- euporie/core/widgets/display.py +12 -15
- euporie/core/widgets/file_browser.py +2 -23
- euporie/core/widgets/forms.py +8 -5
- euporie/core/widgets/inputs.py +13 -70
- euporie/core/widgets/layout.py +2 -1
- euporie/core/widgets/logo.py +49 -0
- euporie/core/widgets/menu.py +10 -8
- euporie/core/widgets/pager.py +6 -10
- euporie/core/widgets/palette.py +6 -6
- euporie/hub/app.py +52 -35
- euporie/notebook/_commands.py +24 -0
- euporie/notebook/_settings.py +107 -0
- euporie/notebook/app.py +49 -171
- euporie/notebook/filters.py +1 -1
- euporie/notebook/tabs/__init__.py +46 -7
- euporie/notebook/tabs/_commands.py +714 -0
- euporie/notebook/tabs/_settings.py +32 -0
- euporie/notebook/tabs/display.py +4 -4
- euporie/notebook/tabs/edit.py +11 -44
- euporie/notebook/tabs/json.py +5 -5
- euporie/notebook/tabs/log.py +1 -18
- euporie/notebook/tabs/notebook.py +11 -660
- euporie/notebook/widgets/_commands.py +11 -0
- euporie/notebook/widgets/_settings.py +19 -0
- euporie/notebook/widgets/side_bar.py +14 -34
- euporie/preview/_settings.py +104 -0
- euporie/preview/app.py +6 -31
- euporie/preview/tabs/notebook.py +6 -72
- euporie/web/__init__.py +1 -0
- euporie/web/tabs/__init__.py +14 -0
- euporie/web/tabs/web.py +11 -6
- euporie/web/widgets/__init__.py +1 -0
- euporie/web/widgets/webview.py +5 -15
- {euporie-2.8.4.dist-info → euporie-2.8.6.dist-info}/METADATA +10 -8
- euporie-2.8.6.dist-info/RECORD +175 -0
- {euporie-2.8.4.dist-info → euporie-2.8.6.dist-info}/WHEEL +1 -1
- {euporie-2.8.4.dist-info → euporie-2.8.6.dist-info}/entry_points.txt +2 -2
- {euporie-2.8.4.dist-info → euporie-2.8.6.dist-info}/licenses/LICENSE +1 -1
- euporie/core/launch.py +0 -64
- euporie/core/terminal.py +0 -522
- euporie-2.8.4.dist-info/RECORD +0 -147
- {euporie-2.8.4.data → euporie-2.8.6.data}/data/share/applications/euporie-console.desktop +0 -0
- {euporie-2.8.4.data → euporie-2.8.6.data}/data/share/applications/euporie-notebook.desktop +0 -0
euporie/core/layout/cache.py
CHANGED
@@ -3,7 +3,7 @@
|
|
3
3
|
from __future__ import annotations
|
4
4
|
|
5
5
|
import logging
|
6
|
-
from functools import
|
6
|
+
from functools import cache
|
7
7
|
from typing import TYPE_CHECKING
|
8
8
|
|
9
9
|
from prompt_toolkit.data_structures import Point
|
@@ -17,7 +17,7 @@ from prompt_toolkit.layout.layout import walk
|
|
17
17
|
from prompt_toolkit.layout.mouse_handlers import MouseHandlers
|
18
18
|
from prompt_toolkit.mouse_events import MouseEvent
|
19
19
|
|
20
|
-
from euporie.core.current import get_app
|
20
|
+
from euporie.core.app.current import get_app
|
21
21
|
from euporie.core.data_structures import DiInt
|
22
22
|
from euporie.core.layout.screen import BoundedWritePosition, Screen
|
23
23
|
|
@@ -313,7 +313,7 @@ class CachedContainer(Container):
|
|
313
313
|
screen.visible_windows_to_write_positions.update(new_wps)
|
314
314
|
screen.height = max(screen.height, self.screen.height)
|
315
315
|
|
316
|
-
@
|
316
|
+
@cache
|
317
317
|
def _wrap_mouse_handler(handler: Callable) -> MouseHandler:
|
318
318
|
def _wrapped(mouse_event: MouseEvent) -> NotImplementedOrNone:
|
319
319
|
# Modify mouse events to reflect position of content
|
@@ -3,7 +3,7 @@
|
|
3
3
|
from __future__ import annotations
|
4
4
|
|
5
5
|
import logging
|
6
|
-
from functools import partial
|
6
|
+
from functools import lru_cache, partial
|
7
7
|
from typing import TYPE_CHECKING
|
8
8
|
|
9
9
|
from prompt_toolkit.application.current import get_app
|
@@ -35,7 +35,8 @@ from euporie.core.layout.controls import DummyControl
|
|
35
35
|
from euporie.core.layout.screen import BoundedWritePosition
|
36
36
|
|
37
37
|
if TYPE_CHECKING:
|
38
|
-
from
|
38
|
+
from collections.abc import Sequence
|
39
|
+
from typing import Any, Callable
|
39
40
|
|
40
41
|
from prompt_toolkit.formatted_text import AnyFormattedText, StyleAndTextTuples
|
41
42
|
from prompt_toolkit.key_binding.key_bindings import (
|
@@ -55,6 +56,42 @@ if TYPE_CHECKING:
|
|
55
56
|
log = logging.getLogger(__name__)
|
56
57
|
|
57
58
|
|
59
|
+
@lru_cache(maxsize=None)
|
60
|
+
class DummyContainer(Container):
|
61
|
+
"""Base class for user interface layout."""
|
62
|
+
|
63
|
+
def __init__(self, width: int = 0, height: int = 0) -> None:
|
64
|
+
"""Define width and height if any."""
|
65
|
+
self.width = width
|
66
|
+
self.height = height
|
67
|
+
|
68
|
+
def reset(self) -> None:
|
69
|
+
"""Reset the state of this container (does nothing)."""
|
70
|
+
|
71
|
+
def preferred_width(self, max_available_width: int) -> Dimension:
|
72
|
+
"""Return a zero-width dimension."""
|
73
|
+
return Dimension.exact(self.width)
|
74
|
+
|
75
|
+
def preferred_height(self, width: int, max_available_height: int) -> Dimension:
|
76
|
+
"""Return a zero-height dimension."""
|
77
|
+
return Dimension.exact(self.height)
|
78
|
+
|
79
|
+
def write_to_screen(
|
80
|
+
self,
|
81
|
+
screen: Screen,
|
82
|
+
mouse_handlers: MouseHandlers,
|
83
|
+
write_position: WritePosition,
|
84
|
+
parent_style: str,
|
85
|
+
erase_bg: bool,
|
86
|
+
z_index: int | None,
|
87
|
+
) -> None:
|
88
|
+
"""Write the actual content to the screen. Does nothing."""
|
89
|
+
|
90
|
+
def get_children(self) -> list[Container]:
|
91
|
+
"""Return an empty list of child :class:`.Container` objects."""
|
92
|
+
return []
|
93
|
+
|
94
|
+
|
58
95
|
class HSplit(ptk_containers.HSplit):
|
59
96
|
"""Several layouts, one stacked above/under the other."""
|
60
97
|
|
@@ -76,6 +113,8 @@ class HSplit(ptk_containers.HSplit):
|
|
76
113
|
style: str | Callable[[], str] = "",
|
77
114
|
) -> None:
|
78
115
|
"""Initialize the HSplit with a cache."""
|
116
|
+
if window_too_small is None:
|
117
|
+
window_too_small = DummyContainer()
|
79
118
|
super().__init__(
|
80
119
|
children=children,
|
81
120
|
window_too_small=window_too_small,
|
@@ -244,6 +283,8 @@ class VSplit(ptk_containers.VSplit):
|
|
244
283
|
style: str | Callable[[], str] = "",
|
245
284
|
) -> None:
|
246
285
|
"""Initialize the VSplit with a cache."""
|
286
|
+
if window_too_small is None:
|
287
|
+
window_too_small = DummyContainer()
|
247
288
|
super().__init__(
|
248
289
|
children=children,
|
249
290
|
window_too_small=window_too_small,
|
@@ -423,17 +464,20 @@ class Window(ptk_containers.Window):
|
|
423
464
|
z_index: int | None,
|
424
465
|
) -> None:
|
425
466
|
"""Write window to screen."""
|
426
|
-
assert isinstance(write_position, BoundedWritePosition)
|
427
467
|
# If dont_extend_width/height was given, then reduce width/height in
|
428
468
|
# WritePosition, if the parent wanted us to paint in a bigger area.
|
429
469
|
# (This happens if this window is bundled with another window in a
|
430
470
|
# HSplit/VSplit, but with different size requirements.)
|
471
|
+
if isinstance(write_position, BoundedWritePosition):
|
472
|
+
bbox = write_position.bbox
|
473
|
+
else:
|
474
|
+
bbox = DiInt(0, 0, 0, 0)
|
431
475
|
write_position = BoundedWritePosition(
|
432
476
|
xpos=write_position.xpos,
|
433
477
|
ypos=write_position.ypos,
|
434
478
|
width=write_position.width,
|
435
479
|
height=write_position.height,
|
436
|
-
bbox=
|
480
|
+
bbox=bbox,
|
437
481
|
)
|
438
482
|
|
439
483
|
if self.dont_extend_width():
|
euporie/core/layout/decor.py
CHANGED
@@ -15,8 +15,8 @@ from prompt_toolkit.layout.dimension import Dimension
|
|
15
15
|
from prompt_toolkit.layout.screen import Char, Screen, WritePosition
|
16
16
|
from prompt_toolkit.mouse_events import MouseEventType
|
17
17
|
|
18
|
+
from euporie.core.app.current import get_app
|
18
19
|
from euporie.core.border import ThinLine
|
19
|
-
from euporie.core.current import get_app
|
20
20
|
from euporie.core.style import ColorPaletteColor
|
21
21
|
|
22
22
|
if TYPE_CHECKING:
|
@@ -341,7 +341,7 @@ class DropShadow(Container):
|
|
341
341
|
) -> None:
|
342
342
|
"""Draw the wrapped container with the additional style."""
|
343
343
|
attr_cache = self.renderer._attrs_for_style
|
344
|
-
if attr_cache is not None:
|
344
|
+
if attr_cache is not None and self.amount:
|
345
345
|
ypos = write_position.ypos
|
346
346
|
xpos = write_position.xpos
|
347
347
|
amount = self.amount
|
euporie/core/layout/mouse.py
CHANGED
euporie/core/layout/print.py
CHANGED
@@ -16,7 +16,8 @@ from prompt_toolkit.layout.dimension import Dimension, to_dimension
|
|
16
16
|
from euporie.core.layout.screen import BoundedWritePosition
|
17
17
|
|
18
18
|
if TYPE_CHECKING:
|
19
|
-
from
|
19
|
+
from collections.abc import Sequence
|
20
|
+
from typing import Callable
|
20
21
|
|
21
22
|
from prompt_toolkit.key_binding.key_bindings import (
|
22
23
|
KeyBindingsBase,
|
euporie/core/layout/scroll.py
CHANGED
@@ -1,7 +1,8 @@
|
|
1
|
-
"""Contains containers which display children at full height
|
1
|
+
"""Contains containers which display children at full height vertically stacked."""
|
2
2
|
|
3
3
|
from __future__ import annotations
|
4
4
|
|
5
|
+
import asyncio
|
5
6
|
import logging
|
6
7
|
from typing import TYPE_CHECKING, cast
|
7
8
|
|
@@ -20,10 +21,10 @@ from prompt_toolkit.mouse_events import MouseEvent, MouseEventType, MouseModifie
|
|
20
21
|
|
21
22
|
from euporie.core.layout.cache import CachedContainer
|
22
23
|
from euporie.core.layout.screen import BoundedWritePosition
|
23
|
-
from euporie.core.utils import run_in_thread_with_context
|
24
24
|
|
25
25
|
if TYPE_CHECKING:
|
26
|
-
from
|
26
|
+
from collections.abc import Sequence
|
27
|
+
from typing import Callable, Literal
|
27
28
|
|
28
29
|
from prompt_toolkit.key_binding.key_bindings import (
|
29
30
|
KeyBindingsBase,
|
@@ -43,6 +44,8 @@ log = logging.getLogger(__name__)
|
|
43
44
|
class ScrollingContainer(Container):
|
44
45
|
"""A scrollable container which renders only the currently visible children."""
|
45
46
|
|
47
|
+
render_info: WindowRenderInfo | None
|
48
|
+
|
46
49
|
def __init__(
|
47
50
|
self,
|
48
51
|
children: Callable[[], Sequence[AnyContainer]] | Sequence[AnyContainer],
|
@@ -65,7 +68,7 @@ class ScrollingContainer(Container):
|
|
65
68
|
self._child_cache: dict[int, CachedContainer] = {}
|
66
69
|
self._children: list[CachedContainer] = []
|
67
70
|
self.refresh_children = True
|
68
|
-
self.pre_rendered =
|
71
|
+
self.pre_rendered: float | None = None
|
69
72
|
|
70
73
|
self._selected_slice = slice(
|
71
74
|
0, 1
|
@@ -89,23 +92,33 @@ class ScrollingContainer(Container):
|
|
89
92
|
|
90
93
|
def pre_render_children(self, width: int, height: int) -> None:
|
91
94
|
"""Render all unrendered children in a background thread."""
|
95
|
+
self.pre_rendered = 0.0
|
96
|
+
children = self.all_children()
|
97
|
+
incr = 1 / len(children)
|
98
|
+
app = get_app()
|
99
|
+
|
100
|
+
def _cb(task: asyncio.Task) -> None:
|
101
|
+
"""Task callback to update pre-rendering percentage."""
|
102
|
+
assert isinstance(self.pre_rendered, float)
|
103
|
+
self.pre_rendered += incr
|
104
|
+
app.invalidate()
|
92
105
|
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
app.invalidate()
|
106
|
+
tasks = set()
|
107
|
+
for child in children:
|
108
|
+
if isinstance(child, CachedContainer):
|
109
|
+
task = app.create_background_task(
|
110
|
+
asyncio.to_thread(child.render, width, height)
|
111
|
+
)
|
112
|
+
task.add_done_callback(_cb)
|
113
|
+
tasks.add(task)
|
114
|
+
|
115
|
+
async def _finish() -> None:
|
116
|
+
await asyncio.gather(*tasks)
|
105
117
|
self.pre_rendered = 1.0
|
106
118
|
app.invalidate()
|
119
|
+
# app.exit()
|
107
120
|
|
108
|
-
|
121
|
+
app.create_background_task(_finish())
|
109
122
|
|
110
123
|
def reset(self) -> None:
|
111
124
|
"""Reset the state of this container and all the children."""
|
@@ -234,7 +247,6 @@ class ScrollingContainer(Container):
|
|
234
247
|
:py:const:`None`
|
235
248
|
|
236
249
|
"""
|
237
|
-
# self.refresh_children = True
|
238
250
|
if n > 0:
|
239
251
|
if (
|
240
252
|
min(self.visible_indices) == 0
|
@@ -252,8 +264,8 @@ class ScrollingContainer(Container):
|
|
252
264
|
if bottom_pos is not None:
|
253
265
|
n = max(
|
254
266
|
n,
|
255
|
-
self.
|
256
|
-
-
|
267
|
+
(bottom_pos + bottom_child.height + self.scrolling)
|
268
|
+
- self.last_write_position.height,
|
257
269
|
)
|
258
270
|
if (
|
259
271
|
bottom_pos + bottom_child.height + self.scrolling + n
|
@@ -362,9 +374,6 @@ class ScrollingContainer(Container):
|
|
362
374
|
# Record children which are currently visible
|
363
375
|
visible_indices = set()
|
364
376
|
|
365
|
-
# Ensure we have the right children
|
366
|
-
all_children = self.all_children()
|
367
|
-
|
368
377
|
# Force the selected children to refresh
|
369
378
|
selected_indices = self.selected_indices
|
370
379
|
self._selected_children: list[CachedContainer] = []
|
@@ -546,19 +555,10 @@ class ScrollingContainer(Container):
|
|
546
555
|
# are partially obscured
|
547
556
|
self.last_write_position = write_position
|
548
557
|
|
549
|
-
# Calculate scrollbar info
|
550
|
-
sizes = self.known_sizes
|
551
|
-
avg_size = sum(sizes.values()) / len(sizes) if sizes else 0
|
552
|
-
n_children = len(all_children)
|
553
|
-
for i in range(n_children):
|
554
|
-
if i not in sizes:
|
555
|
-
sizes[i] = int(avg_size)
|
556
|
-
content_height = max(sum(sizes.values()), 1)
|
557
|
-
|
558
558
|
# Mock up a WindowRenderInfo so we can draw a scrollbar margin
|
559
559
|
self.render_info = WindowRenderInfo(
|
560
560
|
window=cast("Window", self),
|
561
|
-
ui_content=UIContent(line_count=
|
561
|
+
ui_content=UIContent(line_count=max(sum(self.known_sizes.values()), 1)),
|
562
562
|
horizontal_scroll=0,
|
563
563
|
vertical_scroll=self.vertical_scroll,
|
564
564
|
window_width=available_width,
|
@@ -574,7 +574,7 @@ class ScrollingContainer(Container):
|
|
574
574
|
self.scrolling = 0
|
575
575
|
|
576
576
|
# Trigger pre-rendering of children
|
577
|
-
if
|
577
|
+
if self.pre_rendered is None:
|
578
578
|
self.pre_render_children(available_width, available_height)
|
579
579
|
|
580
580
|
@property
|
@@ -727,9 +727,14 @@ class ScrollingContainer(Container):
|
|
727
727
|
def known_sizes(self) -> dict[int, int]:
|
728
728
|
"""A dictionary mapping child indices to height values."""
|
729
729
|
sizes = {}
|
730
|
+
missing = set()
|
730
731
|
for i, child in enumerate(self._children):
|
731
732
|
if isinstance(child, CachedContainer) and child.height:
|
732
733
|
sizes[i] = child.height
|
734
|
+
else:
|
735
|
+
missing.add(i)
|
736
|
+
avg = int(sum(sizes.values()) / len(sizes))
|
737
|
+
sizes.update(dict.fromkeys(missing, avg))
|
733
738
|
return sizes
|
734
739
|
|
735
740
|
def _scroll_up(self) -> None:
|
euporie/core/log.py
CHANGED
@@ -20,12 +20,10 @@ from prompt_toolkit.renderer import (
|
|
20
20
|
from prompt_toolkit.shortcuts.utils import print_formatted_text
|
21
21
|
from prompt_toolkit.styles.pygments import style_from_pygments_cls
|
22
22
|
from prompt_toolkit.styles.style import Style, merge_styles
|
23
|
-
from pygments.styles import get_style_by_name
|
24
23
|
|
25
|
-
from euporie.core.config import add_setting
|
26
24
|
from euporie.core.ft.utils import indent, lex, wrap
|
27
25
|
from euporie.core.io import PseudoTTY
|
28
|
-
from euporie.core.style import LOG_STYLE
|
26
|
+
from euporie.core.style import LOG_STYLE, get_style_by_name
|
29
27
|
from euporie.core.utils import dict_merge
|
30
28
|
|
31
29
|
if TYPE_CHECKING:
|
@@ -42,6 +40,65 @@ log = logging.getLogger(__name__)
|
|
42
40
|
LOG_QUEUE: deque = deque(maxlen=1000)
|
43
41
|
|
44
42
|
|
43
|
+
class BufferedLogs(logging.Handler):
|
44
|
+
"""A handler that collects log records and replays them on exit."""
|
45
|
+
|
46
|
+
def __init__(self, logger: logging.Logger | None = None) -> None:
|
47
|
+
"""Initialize the collector.
|
48
|
+
|
49
|
+
Args:
|
50
|
+
logger: Logger to collect from and replay to. If None, uses root logger.
|
51
|
+
"""
|
52
|
+
super().__init__()
|
53
|
+
self.records: list[logging.LogRecord] = []
|
54
|
+
self._logger = logger or logging.getLogger()
|
55
|
+
self._original_handlers: list[logging.Handler] = []
|
56
|
+
|
57
|
+
def emit(self, record: logging.LogRecord) -> None:
|
58
|
+
"""Store the log record."""
|
59
|
+
self.records.append(record)
|
60
|
+
|
61
|
+
def replay(self) -> None:
|
62
|
+
"""Replay collected logs through the original logger."""
|
63
|
+
for record in self.records:
|
64
|
+
if record.exc_info:
|
65
|
+
# Create a new record to avoid issues with stale exc_info
|
66
|
+
record = logging.LogRecord(
|
67
|
+
record.name,
|
68
|
+
record.levelno,
|
69
|
+
record.pathname,
|
70
|
+
record.lineno,
|
71
|
+
record.msg,
|
72
|
+
record.args,
|
73
|
+
record.exc_info,
|
74
|
+
record.funcName,
|
75
|
+
)
|
76
|
+
self._logger.handle(record)
|
77
|
+
|
78
|
+
def __enter__(self) -> BufferedLogs:
|
79
|
+
"""Store and replace the log handlers."""
|
80
|
+
# Save and remove existing handlers
|
81
|
+
self._original_handlers = self._logger.handlers[:]
|
82
|
+
self._logger.handlers.clear()
|
83
|
+
# Add ourselves as the only handler
|
84
|
+
self._logger.addHandler(self)
|
85
|
+
return self
|
86
|
+
|
87
|
+
def __exit__(
|
88
|
+
self,
|
89
|
+
exc_type: type[BaseException] | None,
|
90
|
+
exc_value: BaseException | None,
|
91
|
+
exc_traceback: TracebackType | None,
|
92
|
+
) -> None:
|
93
|
+
"""Restore the original handlers."""
|
94
|
+
# Remove ourselves
|
95
|
+
self._logger.removeHandler(self)
|
96
|
+
# Restore original handlers
|
97
|
+
self._logger.handlers = self._original_handlers
|
98
|
+
# Replay collected records through original handlers
|
99
|
+
self.replay()
|
100
|
+
|
101
|
+
|
45
102
|
class FtFormatter(logging.Formatter):
|
46
103
|
"""Base class for formatted text logging formatter."""
|
47
104
|
|
@@ -240,12 +297,17 @@ class StdoutFormatter(FtFormatter):
|
|
240
297
|
msg_pad = len(date) + 10
|
241
298
|
msg_pad_1st_line = msg_pad + 1 + len(ref)
|
242
299
|
|
243
|
-
msg_lines =
|
244
|
-
|
245
|
-
|
246
|
-
|
247
|
-
|
248
|
-
|
300
|
+
msg_lines = "\n".join(
|
301
|
+
textwrap.wrap(
|
302
|
+
record.message,
|
303
|
+
width=width,
|
304
|
+
initial_indent=" " * msg_pad_1st_line,
|
305
|
+
replace_whitespace=False,
|
306
|
+
)
|
307
|
+
).split("\n")
|
308
|
+
subsequent_indent = " " * msg_pad
|
309
|
+
for i in range(1, len(msg_lines)):
|
310
|
+
msg_lines[i] = f"{subsequent_indent}{msg_lines[i]}"
|
249
311
|
|
250
312
|
output: StyleAndTextTuples = [
|
251
313
|
("class:pygments.literal.date", date),
|
@@ -416,7 +478,7 @@ def setup_logs(config: Config | None = None) -> None:
|
|
416
478
|
}
|
417
479
|
|
418
480
|
if config is not None:
|
419
|
-
log_file = config.
|
481
|
+
log_file = config.log_file or ""
|
420
482
|
log_file_is_stdout = log_file in {"-", "/dev/stdout"}
|
421
483
|
log_level = config.log_level.upper()
|
422
484
|
|
@@ -433,25 +495,18 @@ def setup_logs(config: Config | None = None) -> None:
|
|
433
495
|
# Configure stdout handler
|
434
496
|
if log_file_is_stdout:
|
435
497
|
stdout_level = log_level
|
436
|
-
elif (app_cls := config.app_cls) is not None and (
|
437
|
-
log_stdout_level := app_cls.log_stdout_level
|
438
|
-
):
|
439
|
-
stdout_level = log_stdout_level.upper()
|
440
498
|
else:
|
441
|
-
stdout_level =
|
499
|
+
stdout_level = config.log_level_stdout.upper()
|
442
500
|
log_config["handlers"]["stdout"]["level"] = stdout_level
|
443
|
-
if syntax_theme := config.
|
501
|
+
if syntax_theme := config.syntax_theme:
|
444
502
|
log_config["handlers"]["stdout"]["pygments_theme"] = syntax_theme
|
445
503
|
|
446
504
|
# Configure euporie logger
|
447
|
-
log_config["loggers"]["euporie"]["level"] =
|
505
|
+
log_config["loggers"]["euporie"]["level"] = log_level
|
448
506
|
|
449
507
|
# Update log_config based on additional config dict provided
|
450
508
|
if config.log_config:
|
451
|
-
|
452
|
-
|
453
|
-
extra_config = json.loads(config.log_config)
|
454
|
-
dict_merge(log_config, extra_config)
|
509
|
+
dict_merge(log_config, config.log_config)
|
455
510
|
|
456
511
|
# Configure the logger
|
457
512
|
# Pytype used TypedDicts to validate the dictionary structure, but I cannot get
|
@@ -463,46 +518,3 @@ def setup_logs(config: Config | None = None) -> None:
|
|
463
518
|
|
464
519
|
# Log uncaught exceptions
|
465
520
|
sys.excepthook = handle_exception
|
466
|
-
|
467
|
-
|
468
|
-
# ################################### Settings ########################################
|
469
|
-
|
470
|
-
|
471
|
-
add_setting(
|
472
|
-
name="log_file",
|
473
|
-
flags=["--log-file"],
|
474
|
-
nargs="?",
|
475
|
-
default="",
|
476
|
-
type_=str,
|
477
|
-
title="the log file path",
|
478
|
-
help_="File path for logs",
|
479
|
-
description="""
|
480
|
-
When set to a file path, the log output will be written to the given path.
|
481
|
-
If no value is given output will be sent to the standard output.
|
482
|
-
""",
|
483
|
-
)
|
484
|
-
|
485
|
-
add_setting(
|
486
|
-
name="log_level",
|
487
|
-
flags=["--log-level"],
|
488
|
-
type_=str,
|
489
|
-
default="warning",
|
490
|
-
title="the log level",
|
491
|
-
help_="Set the log level",
|
492
|
-
choices=["debug", "info", "warning", "error", "critical"],
|
493
|
-
description="""
|
494
|
-
When set, logging events at the given level are emitted.
|
495
|
-
""",
|
496
|
-
)
|
497
|
-
|
498
|
-
add_setting(
|
499
|
-
name="log_config",
|
500
|
-
flags=["--log-config"],
|
501
|
-
type_=str,
|
502
|
-
default=None,
|
503
|
-
title="additional logging configuration",
|
504
|
-
help_="Additional logging configuration",
|
505
|
-
description="""
|
506
|
-
A JSON string specifying additional logging configuration.
|
507
|
-
""",
|
508
|
-
)
|