confease 1.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
confease/__init__.py ADDED
@@ -0,0 +1,65 @@
1
+ from confease.model import (
2
+ CLI,
3
+ DEF,
4
+ ENV,
5
+ ORIGINS,
6
+ SYS,
7
+ USR,
8
+ Confease,
9
+ Confitem,
10
+ )
11
+ from confease.editors import TextEditor
12
+ from confease.parsers import (
13
+ CFG,
14
+ CONF,
15
+ CONFIG,
16
+ CSV,
17
+ INI,
18
+ JSON,
19
+ PARSER_CLASSES,
20
+ PARSERS,
21
+ TOML,
22
+ XML,
23
+ YAML,
24
+ YML,
25
+ Cfg,
26
+ Csv,
27
+ Ini,
28
+ Json,
29
+ Parser,
30
+ Toml,
31
+ Xml,
32
+ Yaml,
33
+ )
34
+
35
+ __all__ = [
36
+ "CFG",
37
+ "CLI",
38
+ "CONF",
39
+ "CONFIG",
40
+ "CSV",
41
+ "DEF",
42
+ "ENV",
43
+ "INI",
44
+ "JSON",
45
+ "ORIGINS",
46
+ "PARSER_CLASSES",
47
+ "PARSERS",
48
+ "SYS",
49
+ "TextEditor",
50
+ "TOML",
51
+ "USR",
52
+ "XML",
53
+ "YAML",
54
+ "YML",
55
+ "Cfg",
56
+ "Confease",
57
+ "Confitem",
58
+ "Csv",
59
+ "Ini",
60
+ "Json",
61
+ "Parser",
62
+ "Toml",
63
+ "Xml",
64
+ "Yaml",
65
+ ]
confease/editors.py ADDED
@@ -0,0 +1,170 @@
1
+ import os
2
+ from pathlib import Path
3
+ import shlex
4
+ import shutil
5
+ import subprocess
6
+ import tempfile
7
+
8
+
9
+ __all__ = [
10
+ "TERMINAL_EDITORS",
11
+ "VISUAL_EDITORS",
12
+ "EDITORS",
13
+ "TextEditor",
14
+ ]
15
+
16
+
17
+ TERMINAL_EDITORS = [["sensible-editor"], ["editor"], ["nano"], ["vim"], ["vi"]]
18
+ VISUAL_WAIT_FLAGS = {
19
+ "code": "--wait",
20
+ "code-insiders": "--wait",
21
+ "codium": "--wait",
22
+ "subl": "--wait",
23
+ "sublime-text": "--wait",
24
+ "sublime_text": "--wait",
25
+ }
26
+ VISUAL_EDITORS = [
27
+ ["code", "--wait"],
28
+ ["code-insiders", "--wait"],
29
+ ["codium", "--wait"],
30
+ ["subl", "--wait"],
31
+ ["sublime-text", "--wait"],
32
+ ["notepadpp.exe"],
33
+ ["notepad.exe"],
34
+ ]
35
+ EDITORS = [*TERMINAL_EDITORS, *VISUAL_EDITORS]
36
+
37
+
38
+ class TextEditor:
39
+ """Small blocking text-editor launcher.
40
+
41
+ ``TextEditor`` resolves an explicit editor command, ``EDITOR``/``VISUAL``,
42
+ or a small list of common terminal and visual editors. Known visual editors
43
+ receive a wait flag when needed so callers can safely read the file after
44
+ the process exits.
45
+ """
46
+
47
+ def __init__(self, editor: str | None = None):
48
+ """Create an editor launcher.
49
+
50
+ Args:
51
+ editor: Optional shell-style editor command, for example
52
+ ``"nano -w"`` or ``"code --wait"``. When omitted, the launcher
53
+ auto-detects an available editor.
54
+
55
+ Raises:
56
+ RuntimeError: If the command string cannot be parsed or is empty.
57
+ """
58
+ self._editor: list[str] | None = None
59
+ self.editor = editor
60
+
61
+ @property
62
+ def editor(self):
63
+ """Return the configured editor command, or None for auto-detection."""
64
+ return self._editor
65
+
66
+ @editor.setter
67
+ def editor(self, value: str | None):
68
+ """Configure an explicit editor command or return to auto-detection."""
69
+ if value is None:
70
+ self._editor = None
71
+ return
72
+
73
+ try:
74
+ command = shlex.split(value)
75
+ except ValueError as exc:
76
+ raise RuntimeError(f"Invalid editor command: {value}") from exc
77
+ if not command:
78
+ raise RuntimeError("Editor command cannot be empty")
79
+ self._editor = command
80
+
81
+ @staticmethod
82
+ def _with_wait_flag(command: list[str]) -> list[str]:
83
+ """Add blocking flags for known visual editors when omitted."""
84
+ executable = Path(command[0]).name.lower()
85
+ wait_flag = VISUAL_WAIT_FLAGS.get(executable)
86
+ if wait_flag and wait_flag not in command[1:]:
87
+ return [command[0], wait_flag, *command[1:]]
88
+ return command
89
+
90
+ @staticmethod
91
+ def _split_env_editor(value: str) -> list[str]:
92
+ """Parse an editor command from an environment variable."""
93
+ try:
94
+ return shlex.split(value)
95
+ except ValueError as exc:
96
+ raise RuntimeError(f"Invalid editor command: {value}") from exc
97
+
98
+ def _editor_command(self) -> list[str]:
99
+ """Return the first available editor command.
100
+
101
+ Raises:
102
+ RuntimeError: If no configured or auto-detected editor is available.
103
+ """
104
+ if self._editor:
105
+ editors = [self._editor]
106
+ else:
107
+ editors = list(EDITORS)
108
+ env_editor = os.environ.get("EDITOR", "").strip() or os.environ.get("VISUAL", "").strip()
109
+ if env_editor:
110
+ editor_command = self._split_env_editor(env_editor)
111
+ if editor_command:
112
+ editors.insert(0, editor_command)
113
+
114
+ for command in editors:
115
+ if shutil.which(command[0]):
116
+ return self._with_wait_flag(command)
117
+ raise RuntimeError(f"Text editor not found '{self._editor}'" if self._editor else "No text editor found")
118
+
119
+ def open(self, path: str | Path) -> str:
120
+ """Open the configured editor for an existing or new file path.
121
+
122
+ Args:
123
+ path: File path to open.
124
+
125
+ Returns:
126
+ The expanded file path as a string after the editor exits.
127
+
128
+ Raises:
129
+ ValueError: If ``path`` is ``None``.
130
+ Exception: If the editor cannot be launched or exits unsuccessfully.
131
+ """
132
+ if path is None:
133
+ raise ValueError("Path not provided")
134
+ try:
135
+ abs_path = Path(path).expanduser()
136
+ with open(abs_path, mode="a+", encoding="utf-8") as message_file:
137
+ subprocess.run([*self._editor_command(), message_file.name], check=True)
138
+ return str(abs_path)
139
+ except OSError as error:
140
+ raise Exception(f"could not open text editor: {error}") from error
141
+ except subprocess.CalledProcessError as error:
142
+ raise Exception(f"text editor process failed: {error}") from error
143
+
144
+ def read(self):
145
+ """Open a temporary file and return its contents after editing.
146
+
147
+ Returns:
148
+ The text written by the editor.
149
+
150
+ Raises:
151
+ Exception: If the editor cannot be launched or exits unsuccessfully.
152
+ """
153
+ try:
154
+ with tempfile.NamedTemporaryFile(
155
+ mode="w+",
156
+ encoding="utf-8",
157
+ ) as message_file:
158
+ subprocess.run([*self._editor_command(), message_file.name], check=True)
159
+ message_file.seek(0)
160
+ return message_file.read()
161
+ except OSError as error:
162
+ raise Exception(f"could not open text editor: {error}") from error
163
+ except subprocess.CalledProcessError as error:
164
+ raise Exception(f"text editor process failed: {error}") from error
165
+
166
+
167
+ def usage():
168
+ editor = TextEditor()
169
+ editor.open("~/.bashrc")
170
+ return editor.read()