hub/venv/lib/python3.7/site-packages/prompt_toolkit/history.py

233 lines
7.0 KiB
Python
Raw Normal View History

"""
Implementations for the history of a `Buffer`.
NOTE: Notice that there is no `DynamicHistory`. This doesn't work well, because
the `Buffer` needs to be able to attach an event handler to the event
when a history entry is loaded. This loading can be done asynchronously
and making the history swappable would probably break this.
"""
import datetime
import os
from abc import ABCMeta, abstractmethod
from threading import Thread
from typing import Callable, Iterable, List, Optional
__all__ = [
"History",
"ThreadedHistory",
"DummyHistory",
"FileHistory",
"InMemoryHistory",
]
class History(metaclass=ABCMeta):
"""
Base ``History`` class.
This also includes abstract methods for loading/storing history.
"""
def __init__(self) -> None:
# In memory storage for strings.
self._loaded = False
self._loaded_strings: List[str] = []
#
# Methods expected by `Buffer`.
#
def load(self, item_loaded_callback: Callable[[str], None]) -> None:
"""
Load the history and call the callback for every entry in the history.
XXX: The callback can be called from another thread, which happens in
case of `ThreadedHistory`.
We can't assume that an asyncio event loop is running, and
schedule the insertion into the `Buffer` using the event loop.
The reason is that the creation of the :class:`.History` object as
well as the start of the loading happens *before*
`Application.run()` is called, and it can continue even after
`Application.run()` terminates. (Which is useful to have a
complete history during the next prompt.)
Calling `get_event_loop()` right here is also not guaranteed to
return the same event loop which is used in `Application.run`,
because a new event loop can be created during the `run`. This is
useful in Python REPLs, where we want to use one event loop for
the prompt, and have another one active during the `eval` of the
commands. (Otherwise, the user can schedule a while/true loop and
freeze the UI.)
"""
if self._loaded:
for item in self._loaded_strings[::-1]:
item_loaded_callback(item)
return
try:
for item in self.load_history_strings():
self._loaded_strings.insert(0, item)
item_loaded_callback(item)
finally:
self._loaded = True
def get_strings(self) -> List[str]:
"""
Get the strings from the history that are loaded so far.
"""
return self._loaded_strings
def append_string(self, string: str) -> None:
" Add string to the history. "
self._loaded_strings.append(string)
self.store_string(string)
#
# Implementation for specific backends.
#
@abstractmethod
def load_history_strings(self) -> Iterable[str]:
"""
This should be a generator that yields `str` instances.
It should yield the most recent items first, because they are the most
important. (The history can already be used, even when it's only
partially loaded.)
"""
while False:
yield
@abstractmethod
def store_string(self, string: str) -> None:
"""
Store the string in persistent storage.
"""
class ThreadedHistory(History):
"""
Wrapper that runs the `load_history_strings` generator in a thread.
Use this to increase the start-up time of prompt_toolkit applications.
History entries are available as soon as they are loaded. We don't have to
wait for everything to be loaded.
"""
def __init__(self, history: History) -> None:
self.history = history
self._load_thread: Optional[Thread] = None
self._item_loaded_callbacks: List[Callable[[str], None]] = []
super().__init__()
def load(self, item_loaded_callback: Callable[[str], None]) -> None:
self._item_loaded_callbacks.append(item_loaded_callback)
# Start the load thread, if we don't have a thread yet.
if not self._load_thread:
def call_all_callbacks(item: str) -> None:
for cb in self._item_loaded_callbacks:
cb(item)
self._load_thread = Thread(
target=self.history.load, args=(call_all_callbacks,)
)
self._load_thread.daemon = True
self._load_thread.start()
def get_strings(self) -> List[str]:
return self.history.get_strings()
def append_string(self, string: str) -> None:
self.history.append_string(string)
# All of the following are proxied to `self.history`.
def load_history_strings(self) -> Iterable[str]:
return self.history.load_history_strings()
def store_string(self, string: str) -> None:
self.history.store_string(string)
def __repr__(self) -> str:
return "ThreadedHistory(%r)" % (self.history,)
class InMemoryHistory(History):
"""
:class:`.History` class that keeps a list of all strings in memory.
"""
def load_history_strings(self) -> Iterable[str]:
return []
def store_string(self, string: str) -> None:
pass
class DummyHistory(History):
"""
:class:`.History` object that doesn't remember anything.
"""
def load_history_strings(self) -> Iterable[str]:
return []
def store_string(self, string: str) -> None:
pass
def append_string(self, string: str) -> None:
# Don't remember this.
pass
class FileHistory(History):
"""
:class:`.History` class that stores all strings in a file.
"""
def __init__(self, filename: str) -> None:
self.filename = filename
super(FileHistory, self).__init__()
def load_history_strings(self) -> Iterable[str]:
strings: List[str] = []
lines: List[str] = []
def add() -> None:
if lines:
# Join and drop trailing newline.
string = "".join(lines)[:-1]
strings.append(string)
if os.path.exists(self.filename):
with open(self.filename, "rb") as f:
for line_bytes in f:
line = line_bytes.decode("utf-8")
if line.startswith("+"):
lines.append(line[1:])
else:
add()
lines = []
add()
# Reverse the order, because newest items have to go first.
return reversed(strings)
def store_string(self, string: str) -> None:
# Save to file.
with open(self.filename, "ab") as f:
def write(t: str) -> None:
f.write(t.encode("utf-8"))
write("\n# %s\n" % datetime.datetime.now())
for line in string.split("\n"):
write("+%s\n" % line)