replbase 0.0.2__tar.gz
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.
- replbase-0.0.2/LICENSE +21 -0
- replbase-0.0.2/PKG-INFO +21 -0
- replbase-0.0.2/README.md +2 -0
- replbase-0.0.2/pyproject.toml +19 -0
- replbase-0.0.2/replbase/__init__.py +5 -0
- replbase-0.0.2/replbase/repl_base.py +310 -0
replbase-0.0.2/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 jmbski
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
replbase-0.0.2/PKG-INFO
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: replbase
|
|
3
|
+
Version: 0.0.2
|
|
4
|
+
Summary: "Combination of other REPL tools into a reusable class that generates a REPL"
|
|
5
|
+
License: MIT
|
|
6
|
+
Author: Joseph Bochinski
|
|
7
|
+
Author-email: stirgejr@gmail.com
|
|
8
|
+
Requires-Python: >=3.12,<4.0
|
|
9
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
12
|
+
Requires-Dist: argcomplete (>=3.5.2,<4.0.0)
|
|
13
|
+
Requires-Dist: prompt-toolkit (>=3.0.48,<4.0.0)
|
|
14
|
+
Requires-Dist: ptpython (>=3.0.29,<4.0.0)
|
|
15
|
+
Requires-Dist: pyyaml (>=6.0.2,<7.0.0)
|
|
16
|
+
Requires-Dist: rich (>=13.9.4,<14.0.0)
|
|
17
|
+
Description-Content-Type: text/markdown
|
|
18
|
+
|
|
19
|
+
# replbase
|
|
20
|
+
A Python tool that combines some features from other REPL packages into one neat reusable base class
|
|
21
|
+
|
replbase-0.0.2/README.md
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = [ "poetry-core",]
|
|
3
|
+
build-backend = "poetry.core.masonry.api"
|
|
4
|
+
|
|
5
|
+
[tool.poetry]
|
|
6
|
+
name = "replbase"
|
|
7
|
+
version = "0.0.2"
|
|
8
|
+
description = "\"Combination of other REPL tools into a reusable class that generates a REPL\""
|
|
9
|
+
authors = [ "Joseph Bochinski <stirgejr@gmail.com>",]
|
|
10
|
+
license = "MIT"
|
|
11
|
+
readme = "README.md"
|
|
12
|
+
|
|
13
|
+
[tool.poetry.dependencies]
|
|
14
|
+
python = "^3.12"
|
|
15
|
+
ptpython = "^3.0.29"
|
|
16
|
+
rich = "^13.9.4"
|
|
17
|
+
prompt-toolkit = "^3.0.48"
|
|
18
|
+
pyyaml = "^6.0.2"
|
|
19
|
+
argcomplete = "^3.5.2"
|
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
"""
|
|
2
|
+
|
|
3
|
+
#######################################################################
|
|
4
|
+
|
|
5
|
+
Module Name: repl_base
|
|
6
|
+
Description: Base class for REPL tools
|
|
7
|
+
Author: Joseph Bochinski
|
|
8
|
+
Date: 2024-12-16
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
#######################################################################
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
# region Imports
|
|
15
|
+
|
|
16
|
+
import argparse
|
|
17
|
+
import os
|
|
18
|
+
import shlex
|
|
19
|
+
|
|
20
|
+
from dataclasses import dataclass
|
|
21
|
+
from typing import Callable, Literal
|
|
22
|
+
|
|
23
|
+
from prompt_toolkit import PromptSession
|
|
24
|
+
from prompt_toolkit.history import FileHistory
|
|
25
|
+
from prompt_toolkit.completion import WordCompleter
|
|
26
|
+
from prompt_toolkit.styles import Style
|
|
27
|
+
from ptpython.repl import embed
|
|
28
|
+
from rich.console import Console
|
|
29
|
+
|
|
30
|
+
# endregion Imports
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
# region Constants
|
|
34
|
+
|
|
35
|
+
ColorSystem = Literal["auto", "standard", "256", "truecolor", "windows"]
|
|
36
|
+
# endregion Constants
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
# region Classes
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@dataclass
|
|
43
|
+
class ReplCommand:
|
|
44
|
+
"""Class definition for a provided CLI REPL command"""
|
|
45
|
+
|
|
46
|
+
command: Callable = None
|
|
47
|
+
help_txt: str = ""
|
|
48
|
+
parser: argparse.ArgumentParser = None
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@dataclass
|
|
52
|
+
class ReplBase:
|
|
53
|
+
"""Dataclass for CLI options"""
|
|
54
|
+
|
|
55
|
+
debug_enabled: bool = None
|
|
56
|
+
"""Debug mode enabled"""
|
|
57
|
+
|
|
58
|
+
title: str = None
|
|
59
|
+
"""Title of the CLI REPL Prompt"""
|
|
60
|
+
|
|
61
|
+
exit_keywords: list[str] = None # field(default_factory=list)
|
|
62
|
+
"""List of strings that cause the REPL to close, defaults to x, q,
|
|
63
|
+
exit, and quit"""
|
|
64
|
+
|
|
65
|
+
init_prompt: str | list[str] = None
|
|
66
|
+
"""Prompt to display at startup"""
|
|
67
|
+
|
|
68
|
+
color_system: ColorSystem = None
|
|
69
|
+
"""Color syste for the rich console"""
|
|
70
|
+
|
|
71
|
+
console: Console = None
|
|
72
|
+
""" Rich Console instance """
|
|
73
|
+
|
|
74
|
+
history: str = None
|
|
75
|
+
"""Path to the prompt history file"""
|
|
76
|
+
|
|
77
|
+
temp_file: str = None
|
|
78
|
+
"""Path to prompt temporary file"""
|
|
79
|
+
|
|
80
|
+
style: dict | Style = None # field(default_factory=dict)
|
|
81
|
+
"""Style for the prompt"""
|
|
82
|
+
|
|
83
|
+
ignore_case: bool = None
|
|
84
|
+
"""Ignore case setting for the WordCompleter instance"""
|
|
85
|
+
|
|
86
|
+
commands: dict[str, ReplCommand] = None # field(default_factory=dict)
|
|
87
|
+
"""Command dictionary for prompt_toolkit. Keys are command names,
|
|
88
|
+
values are the corresponding description/help text"""
|
|
89
|
+
|
|
90
|
+
def __post_init__(self) -> None:
|
|
91
|
+
if isinstance(self.commands, dict):
|
|
92
|
+
for cmd_name, cmd in self.commands.items():
|
|
93
|
+
if isinstance(cmd, dict):
|
|
94
|
+
self.commands[cmd_name] = ReplCommand(**cmd)
|
|
95
|
+
|
|
96
|
+
if self.debug_enabled is None:
|
|
97
|
+
self.debug_enabled = False
|
|
98
|
+
|
|
99
|
+
self.title = self.title or "CLI Tool"
|
|
100
|
+
|
|
101
|
+
self.exit_keywords = self.exit_keywords or ["x", "q", "exit", "quit"]
|
|
102
|
+
|
|
103
|
+
exit_kw_str = ", ".join(
|
|
104
|
+
f'[bold green]"{kw}"[/bold green]' for kw in self.exit_keywords
|
|
105
|
+
)
|
|
106
|
+
exit_kw_pref = "Type one of " if len(self.exit_keywords) > 1 else "Type "
|
|
107
|
+
exit_str = f"[cyan]{exit_kw_pref}{exit_kw_str} to exit[/cyan]"
|
|
108
|
+
|
|
109
|
+
self.init_prompt = self.init_prompt or [
|
|
110
|
+
f"[bold cyan]<<| {self.title} |>>[/bold cyan]",
|
|
111
|
+
exit_str,
|
|
112
|
+
'[cyan]Type [bold green]"help"[/bold green] to view available commands.',
|
|
113
|
+
]
|
|
114
|
+
|
|
115
|
+
self.color_system = self.color_system or "truecolor"
|
|
116
|
+
|
|
117
|
+
self.console = Console(color_system=self.color_system)
|
|
118
|
+
|
|
119
|
+
self.history = self.history or os.path.expanduser(
|
|
120
|
+
"~/.config/.prompt_history"
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
if self.history:
|
|
124
|
+
hist_dir = os.path.dirname(self.history)
|
|
125
|
+
if not os.path.exists(hist_dir):
|
|
126
|
+
os.makedirs(hist_dir, exist_ok=True)
|
|
127
|
+
|
|
128
|
+
self.temp_file = self.temp_file or os.path.expanduser(
|
|
129
|
+
"~/.config/.prompt_tmp"
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
if self.temp_file:
|
|
133
|
+
temp_dir = os.path.dirname(self.temp_file)
|
|
134
|
+
if not os.path.exists(temp_dir):
|
|
135
|
+
os.makedirs(temp_dir, exist_ok=True)
|
|
136
|
+
|
|
137
|
+
self.style = self.style or {
|
|
138
|
+
"prompt": "bold green",
|
|
139
|
+
"": "default",
|
|
140
|
+
}
|
|
141
|
+
if isinstance(self.style, dict):
|
|
142
|
+
self.style = Style.from_dict(self.style)
|
|
143
|
+
|
|
144
|
+
self.apply_def_cmds()
|
|
145
|
+
|
|
146
|
+
def apply_def_cmds(self) -> None:
|
|
147
|
+
"""Add the descriptions for the help and exit commands"""
|
|
148
|
+
base_cmds: dict[str, ReplCommand] = {
|
|
149
|
+
"\\[help, h]": ReplCommand(help_txt="Display this help message again"),
|
|
150
|
+
}
|
|
151
|
+
if self.exit_keywords:
|
|
152
|
+
exit_str = ", ".join(self.exit_keywords)
|
|
153
|
+
base_cmds.update(
|
|
154
|
+
{
|
|
155
|
+
f"\\[{exit_str}]": ReplCommand(help_txt="Exit tool"),
|
|
156
|
+
}
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
self.commands = self.commands or {}
|
|
160
|
+
base_cmds.update(self.commands)
|
|
161
|
+
self.commands = base_cmds
|
|
162
|
+
|
|
163
|
+
def get_cmd_names(self) -> list[str]:
|
|
164
|
+
"""Retrieve names of commands, parsing out the help/exit commands"""
|
|
165
|
+
|
|
166
|
+
help_str = "[help, h]"
|
|
167
|
+
exit_str = f'[{", ".join(self.exit_keywords)}]'
|
|
168
|
+
names: list[str] = [
|
|
169
|
+
name
|
|
170
|
+
for name in self.commands.keys()
|
|
171
|
+
if name not in [help_str, exit_str]
|
|
172
|
+
]
|
|
173
|
+
names.extend(["help", "h"])
|
|
174
|
+
names.extend(self.exit_keywords)
|
|
175
|
+
return names
|
|
176
|
+
|
|
177
|
+
def print(self, *args) -> None:
|
|
178
|
+
"""Shortcut to console.print"""
|
|
179
|
+
self.console.print(*args)
|
|
180
|
+
|
|
181
|
+
def input(self, *args) -> str:
|
|
182
|
+
"""Shortcut to console.input"""
|
|
183
|
+
return self.console.input(*args)
|
|
184
|
+
|
|
185
|
+
def debug(self, *args) -> None:
|
|
186
|
+
"""Print only if debug_enabled == True"""
|
|
187
|
+
if self.debug_enabled:
|
|
188
|
+
self.print(*args)
|
|
189
|
+
|
|
190
|
+
def add_command(
|
|
191
|
+
self,
|
|
192
|
+
cmd_name: str,
|
|
193
|
+
cmd_func: Callable = None,
|
|
194
|
+
help_txt: str = "",
|
|
195
|
+
use_parser: bool = False,
|
|
196
|
+
description: str = "",
|
|
197
|
+
) -> ReplCommand:
|
|
198
|
+
"""Add a command to the REPL
|
|
199
|
+
|
|
200
|
+
Args:
|
|
201
|
+
cmd_name (str): Name of the command
|
|
202
|
+
cmd_func (Callable, optional): Function to execute when called.
|
|
203
|
+
Defaults to None.
|
|
204
|
+
help_txt (str, optional): Help text to display from REPL help command.
|
|
205
|
+
Defaults to "".
|
|
206
|
+
use_parser (bool, optional): If true, adds an argparse.ArgumentParser
|
|
207
|
+
to the new ReplCommand instance. Defaults to False.
|
|
208
|
+
description (str, optional): Optional description for the
|
|
209
|
+
ArgumentParser help text. Defaults to help_txt.
|
|
210
|
+
|
|
211
|
+
Returns:
|
|
212
|
+
ReplCommand: The new ReplCommand instance
|
|
213
|
+
"""
|
|
214
|
+
|
|
215
|
+
new_cmd = ReplCommand(command=cmd_func, help_txt=help_txt)
|
|
216
|
+
if use_parser:
|
|
217
|
+
new_cmd.parser = argparse.ArgumentParser(
|
|
218
|
+
description=description or help_txt
|
|
219
|
+
)
|
|
220
|
+
self.commands[cmd_name] = new_cmd
|
|
221
|
+
return new_cmd
|
|
222
|
+
|
|
223
|
+
def interactive(self, *args, **kwargs) -> None:
|
|
224
|
+
"""Starts an interactive session from within the class"""
|
|
225
|
+
if kwargs:
|
|
226
|
+
globals().update(kwargs)
|
|
227
|
+
embed(globals(), locals())
|
|
228
|
+
|
|
229
|
+
def show_help(self) -> None:
|
|
230
|
+
"""Print out the provided help text"""
|
|
231
|
+
|
|
232
|
+
for cmd_name, cmd in self.commands.items():
|
|
233
|
+
self.print(
|
|
234
|
+
f"[bold green]{cmd_name}:[/bold green] [cyan]{cmd.help_txt}[/cyan]"
|
|
235
|
+
)
|
|
236
|
+
|
|
237
|
+
def run(self) -> None:
|
|
238
|
+
"""Initiates a REPL with the provided configuration"""
|
|
239
|
+
|
|
240
|
+
completer = WordCompleter(
|
|
241
|
+
self.get_cmd_names(), ignore_case=self.ignore_case
|
|
242
|
+
)
|
|
243
|
+
|
|
244
|
+
session = PromptSession(
|
|
245
|
+
completer=completer,
|
|
246
|
+
style=self.style,
|
|
247
|
+
history=FileHistory(self.history),
|
|
248
|
+
tempfile=self.temp_file,
|
|
249
|
+
)
|
|
250
|
+
|
|
251
|
+
if isinstance(self.init_prompt, list):
|
|
252
|
+
for line in self.init_prompt:
|
|
253
|
+
self.print(line)
|
|
254
|
+
else:
|
|
255
|
+
self.print(self.init_prompt)
|
|
256
|
+
|
|
257
|
+
while True:
|
|
258
|
+
try:
|
|
259
|
+
user_input = session.prompt("> ", complete_while_typing=True)
|
|
260
|
+
|
|
261
|
+
if user_input.lower() in ["help", "h"]:
|
|
262
|
+
self.show_help()
|
|
263
|
+
elif user_input.lower() in self.exit_keywords:
|
|
264
|
+
break
|
|
265
|
+
else:
|
|
266
|
+
args = shlex.split(user_input)
|
|
267
|
+
if not args:
|
|
268
|
+
self.print(
|
|
269
|
+
"[bold yellow][WARNING]: No command provided[/bold yellow]"
|
|
270
|
+
)
|
|
271
|
+
continue
|
|
272
|
+
|
|
273
|
+
cmd = self.commands.get(args[0])
|
|
274
|
+
|
|
275
|
+
if not cmd:
|
|
276
|
+
self.print(
|
|
277
|
+
"[bold yellow][WARNING]: Invalid command[/bold yellow]"
|
|
278
|
+
)
|
|
279
|
+
continue
|
|
280
|
+
|
|
281
|
+
cmd_args = args[1:]
|
|
282
|
+
if cmd.command:
|
|
283
|
+
if cmd.parser:
|
|
284
|
+
if cmd_args and cmd_args[0] in [
|
|
285
|
+
"help",
|
|
286
|
+
"h",
|
|
287
|
+
"-h",
|
|
288
|
+
"--help",
|
|
289
|
+
]:
|
|
290
|
+
cmd.parser.print_help()
|
|
291
|
+
continue
|
|
292
|
+
|
|
293
|
+
cmd.command(cmd.parser.parse_args(cmd_args))
|
|
294
|
+
else:
|
|
295
|
+
cmd.command(*cmd_args)
|
|
296
|
+
else:
|
|
297
|
+
self.print(
|
|
298
|
+
"[bold yellow][WARNING]: No function provided for command[/bold yellow]"
|
|
299
|
+
)
|
|
300
|
+
except (EOFError, KeyboardInterrupt):
|
|
301
|
+
self.print("[bold yellow]Exiting REPL...[/bold yellow]")
|
|
302
|
+
break
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
# endregion Classes
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
# region Functions
|
|
309
|
+
|
|
310
|
+
# endregion Functions
|