8000 Don't expect a working event loop in the History classes. · cool-RR/python-prompt-toolkit@bd078af · GitHub
[go: up one dir, main page]

Skip to content

Commit bd078af

Browse files
Don't expect a working event loop in the History classes.
1 parent 8336f69 commit bd078af

File tree

2 files changed

+66
-55
lines changed

2 files changed

+66
-55
lines changed

prompt_toolkit/buffer.py

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -284,14 +284,15 @@ def __init__(self,
284284
# Reset other attributes.
285285
self.reset(document=document)
286286

287-
# Attach callback for new history entries.
288-
def new_history_item(sender: History) -> None:
287+
# Load the history.
288+
def new_history_item(item: str) -> None:
289+
# XXX: Keep in mind that this function can be called in a different
290+
# thread!
289291
# Insert the new string into `_working_lines`.
290-
self._working_lines.insert(0, self.history.get_strings()[0])
291-
self.__working_index += 1
292+
self._working_lines.insert(0, item)
293+
self.__working_index += 1 # Not entirely threadsafe, but probably good enough.
292294

293-
self.history.get_item_loaded_event().add_handler(new_history_item)
294-
self.history.start_loading()
295+
self.history.load(new_history_item)
295296

296297
def __repr__(self) -> str:
297298
if len(self.text) < 15:

prompt_toolkit/history.py

Lines changed: 59 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,8 @@
99
import datetime
1010
import os
1111
from abc import ABCMeta, abstractmethod
12-
from typing import AsyncGenerator, Iterable, List
13-
14-
from prompt_toolkit.application.current import get_app
15-
16-
from .eventloop import generator_to_async_generator
17-
from .utils import Event
12+
from threading import Thread
13+
from typing import Iterable, List, Callable, Optional
1814

1915
__all__ = [
2016
'History',
@@ -33,38 +29,48 @@ class History(metaclass=ABCMeta):
3329
"""
3430
def __init__(self) -> None:
3531
# In memory storage for strings.
36-
self._loading = False
32+
self._loaded = False
3733
self._loaded_strings: List[str] = []
38-
self._item_loaded: Event['History'] = Event(self)
39-
40-
async def _start_loading(self) -> None:
41-
"""
42-
Consume the asynchronous generator: `load_history_strings_async`.
43-
44-
This is only called once, because once the history is loaded, we don't
45-
have to load it again.
46-
"""
47-
def add_string(string: str) -> None:
48-
" Got one string from the asynchronous history generator. "
49-
self._loaded_strings.insert(0, string)
50-
self._item_loaded.fire()
51-
52-
async for item in self.load_history_strings_async():
53-
add_string(item)
5434

5535
#
5636
# Methods expected by `Buffer`.
5737
#
5838

59-
def start_loading(self) -> None:
60-
" Start loading the history. "
61-
if not self._loading:
62-
self._loading = True
63-
get_app().create_background_task(self._start_loading())
64-
65-
def get_item_loaded_event(self) -> Event['History']:
66-
" Event which is triggered when a new item is loaded. "
67-
return self._item_loaded
39+
def load(self, item_loaded_callback: Callable[[str], None]) -> None:
40+
"""
41+
Load the history and call the callback for every entry in the history.
42+
43+
XXX: The callback can be called from another thread, which happens in
44+
case of `ThreadedHistory`.
45+
46+
We can't assume that an asyncio event loop is running, and
47+
schedule the insertion into the `Buffer` using the event loop.
48+
49+
The reason is that the creation of the :class:`.History` object as
50+
well as the start of the loading happens *before*
51+
`Application.run()` is called, and it can continue even after
52+
`Application.run()` terminates. (Which is useful to have a
53+
complete history during the next prompt.)
54+
55+
Calling `get_event_loop()` right here is also not guaranteed to
56+
return the same event loop which is used in `Application.run`,
57+
because a new event loop can be created during the `run`. This is
58+
useful in Python REPLs, where we want to use one event loop for
59+
the prompt, and have another one active during the `eval` of the
60+
commands. (Otherwise, the user can schedule a while/true loop and
61+
freeze the UI.)
62+
"""
63+
if self._loaded:
64+
for item in self._loaded_strings[::-1]:
65+
item_loaded_callback(item)
66+
return
67+
68+
try:
69+
for item in self.load_history_strings():
70+
self._loaded_strings.insert(0, item)
71+
item_loaded_callback(item)
72+
finally:
73+
self._loaded = True
6874

6975
def get_strings(self) -> List[str]:
7076
"""
@@ -93,16 +99,6 @@ def load_history_strings(self) -> Iterable[str]:
9399
while False:
94100
yield
95101

96-
async def load_history_strings_async(self) -> AsyncGenerator[str, None]:
97-
"""
98-
Asynchronous generator for history strings. (Probably, you won't have
99-
to override this.)
100-
101-
This is an asynchronous generator of `str` objects.
102-
"""
103-
for item in self.load_history_strings():
104-
yield item
105-
106102
@abstractmethod
107103
def store_string(self, string: str) -> None:
108104
"""
@@ -120,15 +116,29 @@ class ThreadedHistory(History):
120116
"""
121117
def __init__(self, history: History) -> None:
122118
self.history = history
119+
self._load_thread: Optional[Thread] = None
120+
self._item_loaded_callbacks: List[Callable[[str], None]] = []
123121
super().__init__()
124122

125-
async def load_history_strings_async(self) -> AsyncGenerator[str, None]:
126-
"""
127-
Asynchronous generator of completions.
128-
This yields both Future and Completion objects.
129-
"""
130-
async for item in generator_to_async_generator(self.history.load_history_strings):
131-
yield item
123+
def load(self, item_loaded_callback: Callable[[str], None]) -> None:
124+
self._item_loaded_callbacks.append(item_loaded_callback)
125+
126+
# Start the load thread, if we don't have a thread yet.
127+
if not self._load_thread:
128+
def call_all_callbacks(item: str) -> None:
129+
for cb in self._item_loaded_callbacks:
130+
cb(item)
131+
132+
self._load_thread = Thread(target=self.history.load, args=(call_all_callbacks, ))
133+
print(self._load_thread.daemon)
134+
self._load_thread.daemon = True
135+
self._load_thread.start()
136+
137+
def get_strings(self) -> List[str]:
138+
return self.history.get_strings()
139+
140+
def append_string(self, string: str) -> None:
141+
self.history.append_string(string)
132142

133143
# All of the following are proxied to `self.history`.
134144

0 commit comments

Comments
 (0)
0