Skip to content

Commit c13ff0f

Browse files
committed
Populate readline history from unpickled history
1 parent 69308f4 commit c13ff0f

File tree

3 files changed

+53
-62
lines changed

3 files changed

+53
-62
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,8 @@
4242
only tracks valid input, while readline history captures all input.
4343
* History is now persisted in a binary format, not plain text format. Previous history files are destroyed
4444
on first launch of a `cmd2` based app of version 0.9.13 or higher.
45+
* HistoryItem class is no longer a subclass of `str`. If you are directly accessing the `.history` attribute
46+
of a `cmd2` based app, you will need to update your code to use `.history.get(1).statement.raw` instead.
4547
* **Python 3.4 EOL notice**
4648
* Python 3.4 reached its [end of life](https://www.python.org/dev/peps/pep-0429/) on March 18, 2019
4749
* This is the last release of `cmd2` which will support Python 3.4

cmd2/cmd2.py

Lines changed: 23 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -404,9 +404,8 @@ def __init__(self, completekey: str = 'tab', stdin=None, stdout=None, persistent
404404

405405
# Commands to exclude from the history command
406406
# initialize history
407-
self.persistent_history_file = persistent_history_file
408407
self.persistent_history_length = persistent_history_length
409-
self._initialize_history()
408+
self._initialize_history(persistent_history_file)
410409
self.exclude_from_history = '''history edit eof eos'''.split()
411410

412411
# Command aliases and macros
@@ -3448,7 +3447,7 @@ def do_history(self, args: argparse.Namespace) -> None:
34483447
for hi in history:
34493448
self.poutput(hi.pr(script=args.script, expanded=args.expanded, verbose=args.verbose))
34503449

3451-
def _initialize_history(self):
3450+
def _initialize_history(self, hist_file):
34523451
"""Initialize history using history related attributes
34533452
34543453
This function can determine whether history is saved in the prior text-based
@@ -3462,43 +3461,46 @@ def _initialize_history(self):
34623461
"""
34633462
self.history = History()
34643463
# with no persistent history, nothing else in this method is relevant
3465-
if not self.persistent_history_file:
3464+
if not hist_file:
3465+
self.persistent_history_file = hist_file
34663466
return
34673467

3468-
self.persistent_history_file = os.path.expanduser(self.persistent_history_file)
3468+
hist_file = os.path.expanduser(hist_file)
34693469

34703470
# first we try and unpickle the history file
34713471
history = History()
34723472
try:
3473-
with open(self.persistent_history_file, 'rb') as fobj:
3473+
with open(hist_file, 'rb') as fobj:
34743474
history = pickle.load(fobj)
34753475
except (FileNotFoundError, KeyError, EOFError):
34763476
pass
34773477
except IsADirectoryError:
34783478
msg = "persistent history file '{}' is a directory"
3479-
self.perror(msg.format(self.persistent_history_file))
3479+
self.perror(msg.format(hist_file))
3480+
return
34803481
except OSError as ex:
34813482
msg = "can not read persistent history file '{}': {}"
3482-
self.perror(msg.format(self.persistent_history_file, ex), traceback_war=False)
3483+
self.perror(msg.format(hist_file, ex), traceback_war=False)
3484+
return
34833485

34843486
self.history = history
3485-
3486-
# trim history to length and ensure it's writable
3487-
# history.truncate(maxlen)
3488-
# try:
3489-
# # open with append so it doesn't truncate the file
3490-
# with open(histfile, 'ab') as fobj:
3491-
# self.persistent_history_file = histfile
3492-
# except OSError as ex:
3493-
# msg = "can not write persistent history file '{}': {}"
3494-
# self.perror(msg.format(histfile, ex), traceback_war=False)
3487+
self.persistent_history_file = hist_file
3488+
3489+
# populate readline history
3490+
if rl_type != RlType.NONE:
3491+
last = None
3492+
for item in history:
3493+
# readline only adds a single entry for multiple sequential identical commands
3494+
# so we emulate that behavior here
3495+
if item.statement.raw != last:
3496+
readline.add_history(item.statement.raw)
3497+
last = item.statement.raw
34953498

34963499
# register a function to write history at save
34973500
# if the history file is in plain text format from 0.9.12 or lower
34983501
# this will fail, and the history in the plain text file will be lost
3499-
3500-
#import atexit
3501-
#atexit.register(self._persist_history_on_exit)
3502+
import atexit
3503+
atexit.register(self._persist_history_on_exit)
35023504

35033505
def _persist_history_on_exit(self):
35043506
"""write history out to the history file"""
@@ -3509,7 +3511,6 @@ def _persist_history_on_exit(self):
35093511
try:
35103512
with open(self.persistent_history_file, 'wb') as fobj:
35113513
pickle.dump(self.history, fobj)
3512-
35133514
except OSError as ex:
35143515
msg = "can not write persistent history file '{}': {}"
35153516
self.perror(msg.format(self.persistent_history_file, ex), traceback_war=False)

tests/test_history.py

Lines changed: 28 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -458,46 +458,6 @@ def hist_file():
458458
except FileNotFoundError:
459459
pass
460460

461-
def test_existing_history_file(hist_file, capsys):
462-
463-
# Create the history file before making cmd2 app
464-
with open(hist_file, 'w'):
465-
pass
466-
467-
# Create a new cmd2 app
468-
cmd2.Cmd(persistent_history_file=hist_file)
469-
_, err = capsys.readouterr()
470-
471-
# Make sure there were no errors
472-
assert err == ''
473-
474-
# Unregister the call to write_history_file that cmd2 did
475-
## TODO atexit.unregister(readline.write_history_file)
476-
477-
# Remove created history file
478-
#os.remove(hist_file)
479-
480-
def test_new_history_file(hist_file, capsys):
481-
482-
# Remove any existing history file
483-
try:
484-
os.remove(hist_file)
485-
except OSError:
486-
pass
487-
488-
# Create a new cmd2 app
489-
cmd2.Cmd(persistent_history_file=hist_file)
490-
_, err = capsys.readouterr()
491-
492-
# Make sure there were no errors
493-
assert err == ''
494-
495-
# Unregister the call to write_history_file that cmd2 did
496-
### TODO atexit.unregister(readline.write_history_file)
497-
498-
# Remove created history file
499-
#os.remove(hist_file)
500-
501461
def test_bad_history_file_path(capsys, request):
502462
# Use a directory path as the history file
503463
test_dir = os.path.dirname(request.module.__file__)
@@ -531,3 +491,31 @@ def test_history_file_conversion_no_truncate_on_init(hist_file, capsys):
531491
assert histlist[0]== 'help\n'
532492
assert histlist[1] == 'alias\n'
533493
assert histlist[2] == 'alias create s shortcuts\n'
494+
495+
def test_history_populates_readline(hist_file):
496+
# - create a cmd2 with persistent history
497+
app = cmd2.Cmd(persistent_history_file=hist_file)
498+
run_cmd(app, 'help')
499+
run_cmd(app, 'shortcuts')
500+
run_cmd(app, 'shortcuts')
501+
run_cmd(app, 'alias')
502+
503+
# call the private method which is registered to write history at exit
504+
app._persist_history_on_exit()
505+
# - create a new cmd2 with persistent history
506+
app = cmd2.Cmd(persistent_history_file=hist_file)
507+
508+
assert len(app.history) == 4
509+
assert app.history.get(1).statement.raw == 'help'
510+
assert app.history.get(2).statement.raw == 'shortcuts'
511+
assert app.history.get(3).statement.raw == 'shortcuts'
512+
assert app.history.get(4).statement.raw == 'alias'
513+
514+
# readline only adds a single entry for multiple sequential identical commands
515+
# so we check to make sure that cmd2 populated the readline history
516+
# using the same rules
517+
from cmd2.rl_utils import readline
518+
assert readline.get_current_history_length() == 3
519+
assert readline.get_history_item(1) == 'help'
520+
assert readline.get_history_item(2) == 'shortcuts'
521+
assert readline.get_history_item(3) == 'alias'

0 commit comments

Comments
 (0)