Repository: python-cmd2/cmd2

Issue:
persistent history file should create directory if needed
When specifying a persistent history file, it would be nice for it to create the parent directories for you. For example, if you have a suite of tools that will store configuration and history in a common directory in the user home directory. 

For example, have a directory `/home/username/.myapp/` be created automatically if persistent history file is set to `/home/username/.myapp/myhistory`.

Discussion:

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 37999bbf..57b321fc 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,6 +1,7 @@
 ## 0.9.16 (TBD, 2019)
 * Enhancements
     * Raise `TypeError` if trying to set choices/completions on argparse action that accepts no arguments
+    * Create directory for the persistent history file if it does not already exist
 
 ## 0.9.15 (July 24, 2019)
 * Bug Fixes
diff --git a/cmd2/cmd2.py b/cmd2/cmd2.py
index adadbdf8..7c529acc 100644
--- a/cmd2/cmd2.py
+++ b/cmd2/cmd2.py
@@ -3603,15 +3603,25 @@ class Cmd(cmd.Cmd):
 
         hist_file = os.path.abspath(os.path.expanduser(hist_file))
 
-        # first we try and unpickle the history file
-        history = History()
         # on Windows, trying to open a directory throws a permission
         # error, not a `IsADirectoryError`. So we'll check it ourselves.
         if os.path.isdir(hist_file):
-            msg = "persistent history file '{}' is a directory"
+            msg = "Persistent history file '{}' is a directory"
             self.perror(msg.format(hist_file))
             return
 
+        # Create the directory for the history file if it doesn't already exist
+        hist_file_dir = os.path.dirname(hist_file)
+        try:
+            os.makedirs(hist_file_dir, exist_ok=True)
+        except OSError as ex:
+            msg = "Error creating persistent history file directory '{}': {}".format(hist_file_dir, ex)
+            self.pexcept(msg)
+            return
+
+        # first we try and unpickle the history file
+        history = History()
+
         try:
             with open(hist_file, 'rb') as fobj:
                 history = pickle.load(fobj)
@@ -3619,7 +3629,7 @@ class Cmd(cmd.Cmd):
             # If any non-operating system error occurs when attempting to unpickle, just use an empty history
             pass
         except OSError as ex:
-            msg = "can not read persistent history file '{}': {}"
+            msg = "Can not read persistent history file '{}': {}"
             self.pexcept(msg.format(hist_file, ex))
             return
 
@@ -3655,7 +3665,7 @@ class Cmd(cmd.Cmd):
             with open(self.persistent_history_file, 'wb') as fobj:
                 pickle.dump(self.history, fobj)
         except OSError as ex:
-            msg = "can not write persistent history file '{}': {}"
+            msg = "Can not write persistent history file '{}': {}"
             self.pexcept(msg.format(self.persistent_history_file, ex))
 
     def _generate_transcript(self, history: List[Union[HistoryItem, str]], transcript_file: str) -> None:


Test patch:
diff --git a/tests/test_history.py b/tests/test_history.py
index 2da34cfd..9752ed07 100644
--- a/tests/test_history.py
+++ b/tests/test_history.py
@@ -638,6 +638,36 @@ def test_history_file_is_directory(capsys):
         _, err = capsys.readouterr()
         assert 'is a directory' in err
 
+def test_history_can_create_directory(mocker):
+    # Mock out atexit.register so the persistent file doesn't written when this function
+    # exists because we will be deleting the directory it needs to go to.
+    mock_register = mocker.patch('atexit.register')
+
+    # Create a temp path for us to use and let it get deleted
+    with tempfile.TemporaryDirectory() as test_dir:
+        pass
+    assert not os.path.isdir(test_dir)
+
+    # Add some subdirectories for the complete history file directory
+    hist_file_dir = os.path.join(test_dir, 'subdir1', 'subdir2')
+    hist_file = os.path.join(hist_file_dir, 'hist_file')
+
+    # Make sure cmd2 creates the history file directory
+    cmd2.Cmd(persistent_history_file=hist_file)
+    assert os.path.isdir(hist_file_dir)
+
+    # Cleanup
+    os.rmdir(hist_file_dir)
+
+def test_history_cannot_create_directory(mocker, capsys):
+    mock_open = mocker.patch('os.makedirs')
+    mock_open.side_effect = OSError
+
+    hist_file_path = os.path.join('fake_dir', 'file')
+    cmd2.Cmd(persistent_history_file=hist_file_path)
+    _, err = capsys.readouterr()
+    assert 'Error creating persistent history file directory' in err
+
 def test_history_file_permission_error(mocker, capsys):
     mock_open = mocker.patch('builtins.open')
     mock_open.side_effect = PermissionError
@@ -645,7 +675,7 @@ def test_history_file_permission_error(mocker, capsys):
     cmd2.Cmd(persistent_history_file='/tmp/doesntmatter')
     out, err = capsys.readouterr()
     assert not out
-    assert 'can not read' in err
+    assert 'Can not read' in err
 
 def test_history_file_conversion_no_truncate_on_init(hist_file, capsys):
     # make sure we don't truncate the plain text history file on init
@@ -720,4 +750,4 @@ def test_persist_history_permission_error(hist_file, mocker, capsys):
     app._persist_history()
     out, err = capsys.readouterr()
     assert not out
-    assert 'can not write' in err
+    assert 'Can not write' in err