1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108
|
import os
import tempfile
import time
from pathlib import Path
from typing import Sequence, Union
class EditorError(Exception):
"""Root editor-related error.
Root exception raised by all exceptions in :func:`.edit`.
"""
class EditorDidNotSaveError(EditorError):
"""User did not save upon exiting :func:`.edit`."""
class EditorDidNotChangeError(EditorError):
"""User did not edit file contents in :func:`.edit`."""
class EditorNotFoundError(EditorError):
"""Could not find a valid text editor for :func`.edit`."""
def edit(
initial_text: str = "",
*,
fallback_editors: Sequence[str] = ("nano", "vim", "notepad", "gedit"),
editor_args: Sequence[str] = (),
path: Union[str, Path] = "",
encoding: str = "utf-8",
save: bool = True,
required: bool = True,
) -> str:
"""Get text input from a user by launching their default text editor.
Parameters
----------
initial_text: str
Initial text to populate the text file with.
fallback_editors: Sequence[str]
If the text editor cannot be determined from the environment variable ``EDITOR``, attempt to use these text editors in the order provided.
editor_args: Sequence[str]
Additional CLI arguments that are passed along to the editor-launch command.
path: Union[str, Path]
If specified, the path to the file that should be opened.
Text editors typically display this, so a custom path may result in a better user-interface.
Defaults to a temporary text file.
encoding: str
File encoding to use.
save: bool
**Require** the user to save before exiting the editor. Otherwise raises :exc:`EditorDidNotSaveError`.
required: bool
**Require** for the saved text to be different from ``initial_text``. Otherwise raises :exc:`EditorDidNotChangeError`.
Raises
------
EditorError
Base editor error exception. Explicitly raised if editor subcommand
returned a non-zero exit code.
EditorNotFoundError
A suitable text editor could not be found.
EditorDidNotSaveError
The user exited the text-editor without saving and ``save=True``.
EditorDidNotChangeError
The user did not change the file contents and ``required=True``.
Returns
-------
str
The resulting text that was saved by the text editor.
"""
import shutil
import subprocess
for editor in (os.environ.get("EDITOR"), *fallback_editors):
if editor and shutil.which(editor):
break
else:
raise EditorNotFoundError
if path:
path = Path(path)
path.parent.mkdir(exist_ok=True, parents=True)
else:
path = Path(tempfile.NamedTemporaryFile(suffix=".txt", mode="w", delete=False).name)
path.write_text(initial_text, encoding=encoding)
past_time = time.time() - 5 # arbitrarily set time to 5 seconds ago; some systems only have 1 second precision.
os.utime(path, (past_time, past_time)) # Set access and modification time
start_stat = path.stat()
try:
subprocess.check_call([editor, path, *editor_args])
end_stat = path.stat()
if save and end_stat.st_mtime <= start_stat.st_mtime:
raise EditorDidNotSaveError
edited_text = path.read_text(encoding=encoding)
except subprocess.CalledProcessError as e:
raise EditorError(f"{editor} exited with status {e.returncode}") from e
finally:
path.unlink(missing_ok=True)
if required and edited_text == initial_text:
raise EditorDidNotChangeError
return edited_text
|