File: _edit.py

package info (click to toggle)
python-cyclopts 3.12.0-3
  • links: PTS, VCS
  • area: main
  • in suites: sid
  • size: 3,288 kB
  • sloc: python: 11,445; makefile: 24
file content (108 lines) | stat: -rw-r--r-- 3,632 bytes parent folder | download | duplicates (2)
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