7
7
loading can be done asynchronously and making the history swappable would
8
8
probably break this.
9
9
"""
10
+ import asyncio
10
11
import datetime
11
12
import os
13
+ import threading
12
14
from abc import ABCMeta , abstractmethod
13
- from threading import Thread
14
- from typing import Callable , Iterable , List , Optional
15
+ from typing import AsyncGenerator , Iterable , List , Optional , Sequence
15
16
16
17
__all__ = [
17
18
"History" ,
@@ -32,57 +33,43 @@ class History(metaclass=ABCMeta):
32
33
def __init__ (self ) -> None :
33
34
# In memory storage for strings.
34
35
self ._loaded = False
36
+
37
+ # History that's loaded already, in reverse order. Latest, most recent
38
+ # item first.
35
39
self ._loaded_strings : List [str ] = []
36
40
37
41
#
38
42
# Methods expected by `Buffer`.
39
43
#
40
44
41
- def load (self , item_loaded_callback : Callable [[ str ] , None ]) -> None :
45
+ async def load (self ) -> AsyncGenerator [ str , None ]:
42
46
"""
43
- Load the history and call the callback for every entry in the history.
44
-
45
- XXX: The callback can be called from another thread, which happens in
46
- case of `ThreadedHistory`.
47
-
48
- We can't assume that an asyncio event loop is running, and
49
- schedule the insertion into the `Buffer` using the event loop.
50
-
51
- The reason is that the creation of the :class:`.History` object as
52
- well as the start of the loading happens *before*
53
- `Application.run()` is called, and it can continue even after
54
- `Application.run()` terminates. (Which is useful to have a
55
- complete history during the next prompt.)
56
-
57
- Calling `get_event_loop()` right here is also not guaranteed to
58
- return the same event loop which is used in `Application.run`,
59
- because a new event loop can be created during the `run`. This is
60
- useful in Python REPLs, where we want to use one event loop for
61
- the prompt, and have another one active during the `eval` of the
62
- commands. (Otherwise, the user can schedule a while/true loop and
63
- freeze the UI.)
47
+ Load the history and yield all the entries in reverse order (latest,
48
+ most recent history entry first).
49
+
50
+ This method can be called multiple times from the `Buffer` to
51
+ repopulate the history when prompting for a new input. So we are
52
+ responsible here for both caching, and making sure that strings that
53
+ were were appended to the history will be incorporated next time this
54
+ method is called.
64
55
"""
65
- if self ._loaded :
66
- for item in self ._loaded_strings [::- 1 ]:
67
- item_loaded_callback (item )
68
- return
69
-
70
- try :
71
- for item in self .load_history_strings ():
72
- self ._loaded_strings .insert (0 , item )
73
- item_loaded_callback (item )
74
- finally :
56
+ if not self ._loaded :
57
+ self ._loaded_strings = list (self .load_history_strings ())
75
58
self ._loaded = True
76
59
60
+ for item in self ._loaded_strings :
61
+ yield item
62
+
77
63
def get_strings (self ) -> List [str ]:
78
64
"""
79
65
Get the strings from the history that are loaded so far.
66
+ (In order. Oldest item first.)
80
67
"""
81
- return self ._loaded_strings
68
+ return self ._loaded_strings [:: - 1 ]
82
69
83
70
def append_string (self , string : str ) -> None :
84
71
" Add string to the history. "
85
- self ._loaded_strings .append ( string )
72
+ self ._loaded_strings .insert ( 0 , string )
86
73
self .store_string (string )
87
74
88
75
#
@@ -110,40 +97,99 @@ def store_string(self, string: str) -> None:
110
97
111
98
class ThreadedHistory (History ):
112
99
"""
113
- Wrapper that runs the `load_history_strings` generator in a thread.
100
+ Wrapper around `History` implementations that run the `load()` generator in
101
+ a thread.
114
102
115
103
Use this to increase the start-up time of prompt_toolkit applications.
116
104
History entries are available as soon as they are loaded. We don't have to
117
105
wait for everything to be loaded.
118
106
"""
119
107
120
108
def __init__ (self , history : History ) -> None :
121
- self .history = history
122
- self ._load_thread : Optional [Thread ] = None
123
- self ._item_loaded_callbacks : List [Callable [[str ], None ]] = []
124
109
super ().__init__ ()
125
110
126
- def load (self , item_loaded_callback : Callable [[str ], None ]) -> None :
127
- self ._item_loaded_callbacks .append (item_loaded_callback )
111
+ self .history = history
128
112
129
- # Start the load thread, if we don't have a thread yet.
130
- if not self ._load_thread :
113
+ self ._load_thread : Optional [threading .Thread ] = None
131
114
132
- def call_all_callbacks ( item : str ) -> None :
133
- for cb in self . _item_loaded_callbacks :
134
- cb ( item )
115
+ # Lock for accessing/manipulating `_loaded_strings` and `_loaded`
116
+ # together in a consistent state.
117
+ self . _lock = threading . Lock ( )
135
118
136
- self ._load_thread = Thread (
137
- target = self .history .load , args = (call_all_callbacks ,)
119
+ # Events created by each `load()` call. Used to wait for new history
120
+ # entries from the loader thread.
121
+ self ._string_load_events : List [threading .Event ] = []
122
+
123
+ async def load (self ) -> AsyncGenerator [str , None ]:
124
+ """
125
+ Like `History.load(), but call `self.load_history_strings()` in a
126
+ background thread.
127
+ """
128
+ # Start the load thread, if this is called for the first time.
129
+ if not self ._load_thread :
130
+ self ._load_thread = threading .Thread (
131
+ target = self ._in_load_thread ,
132
+ daemon = True ,
138
133
)
139
- self ._load_thread .daemon = True
140
134
self ._load_thread .start ()
141
135
142
- def get_strings (self ) -> List [str ]:
143
- return self .history .get_strings ()
136
+ # Consume the `_loaded_strings` list, using asyncio.
137
+ loop = asyncio .get_event_loop ()
138
+
139
+ # Create threading Event so that we can wait for new items.
140
+ event = threading .Event ()
141
+ event .set ()
142
+ self ._string_load_events .append (event )
143
+
144
+ items_yielded = 0
145
+
146
+ try :
147
+ while True :
148
+ # Wait for new items to be available.
149
+ await loop .run_in_executor (None , event .wait )
150
+
151
+ # Read new items (in lock).
152
+ await loop .run_in_executor (None , self ._lock .acquire )
153
+ try :
154
+ new_items = self ._loaded_strings [items_yielded :]
155
+ done = self ._loaded
156
+ event .clear ()
157
+ finally :
158
+ self ._lock .release ()
159
+
160
+ items_yielded += len (new_items )
161
+
162
+ for item in new_items :
163
+ yield item
164
+
165
+ if done :
166
+ break
167
+ finally :
168
+ self ._string_load_events .remove (event )
169
+
170
+ def _in_load_thread (self ) -> None :
171
+ try :
172
+ # Start with an empty list. In case `append_string()` was called
173
+ # before `load()` happened. Then `.store_string()` will have
174
+ # written these entries back to disk and we will reload it.
175
+ self ._loaded_strings = []
176
+
177
+ for item in self .history .load_history_strings ():
178
+ with self ._lock :
179
+ self ._loaded_strings .append (item )
180
+
181
+ for event in self ._string_load_events :
182
+ event .set ()
183
+ finally :
184
+ with self ._lock :
185
+ self ._loaded = True
186
+ for event in self ._string_load_events :
187
+ event .set ()
144
188
145
189
def append_string (self , string : str ) -> None :
146
- self .history .append_string (string )
190
+ with self ._lock :
191
+ self ._loaded_strings .insert (0 , string )
192
+ self .store_string (string )
147
193
148
194
# All of the following are proxied to `self.history`.
149
195
@@ -160,13 +206,25 @@ def __repr__(self) -> str:
160
206
class InMemoryHistory (History ):
161
207
"""
162
208
:class:`.History` class that keeps a list of all strings in memory.
209
+
210
+ In order to prepopulate the history, it's possible to call either
211
+ `append_string` for all items or pass a list of strings to `__init__` here.
163
212
"""
164
213
214
+ def __init__ (self , history_strings : Optional [Sequence [str ]] = None ) -> None :
215
+ super ().__init__ ()
216
+ # Emulating disk storage.
217
+ if history_strings is None :
218
+ self ._storage = []
219
+ else :
220
+ self ._storage = list (history_strings )
221
+
165
222
def load_history_strings (self ) -> Iterable [str ]:
166
- return []
223
+ for item in self ._storage [::- 1 ]:
224
+ yield item
167
225
168
226
def store_string (self , string : str ) -> None :
169
- pass
227
+ self . _storage . append ( string )
170
228
171
229
172
230
class DummyHistory (History ):
0 commit comments