python-tty 0.1.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.
- python_tty/__init__.py +15 -0
- python_tty/commands/__init__.py +24 -0
- python_tty/commands/core.py +119 -0
- python_tty/commands/decorators.py +58 -0
- python_tty/commands/examples/__init__.py +8 -0
- python_tty/commands/examples/root_commands.py +33 -0
- python_tty/commands/examples/sub_commands.py +11 -0
- python_tty/commands/general.py +107 -0
- python_tty/commands/mixins.py +52 -0
- python_tty/commands/registry.py +185 -0
- python_tty/config/__init__.py +9 -0
- python_tty/config/config.py +35 -0
- python_tty/console_factory.py +100 -0
- python_tty/consoles/__init__.py +16 -0
- python_tty/consoles/core.py +140 -0
- python_tty/consoles/decorators.py +42 -0
- python_tty/consoles/examples/__init__.py +8 -0
- python_tty/consoles/examples/root_console.py +34 -0
- python_tty/consoles/examples/sub_console.py +34 -0
- python_tty/consoles/loader.py +14 -0
- python_tty/consoles/manager.py +146 -0
- python_tty/consoles/registry.py +102 -0
- python_tty/exceptions/__init__.py +7 -0
- python_tty/exceptions/console_exception.py +12 -0
- python_tty/executor/__init__.py +10 -0
- python_tty/executor/executor.py +335 -0
- python_tty/executor/models.py +38 -0
- python_tty/frontends/__init__.py +0 -0
- python_tty/meta/__init__.py +0 -0
- python_tty/ui/__init__.py +13 -0
- python_tty/ui/events.py +55 -0
- python_tty/ui/output.py +102 -0
- python_tty/utils/__init__.py +13 -0
- python_tty/utils/table.py +126 -0
- python_tty/utils/tokenize.py +45 -0
- python_tty/utils/ui_logger.py +17 -0
- python_tty-0.1.0.dist-info/METADATA +66 -0
- python_tty-0.1.0.dist-info/RECORD +40 -0
- python_tty-0.1.0.dist-info/WHEEL +5 -0
- python_tty-0.1.0.dist-info/top_level.txt +1 -0
python_tty/__init__.py
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
from python_tty.config import Config
|
|
2
|
+
from python_tty.console_factory import ConsoleFactory
|
|
3
|
+
from python_tty.ui.events import UIEvent, UIEventLevel, UIEventListener, UIEventSpeaker
|
|
4
|
+
from python_tty.ui.output import proxy_print
|
|
5
|
+
|
|
6
|
+
__all__ = [
|
|
7
|
+
"UIEvent",
|
|
8
|
+
"UIEventLevel",
|
|
9
|
+
"UIEventListener",
|
|
10
|
+
"UIEventSpeaker",
|
|
11
|
+
"ConsoleFactory",
|
|
12
|
+
"Config",
|
|
13
|
+
"proxy_print",
|
|
14
|
+
]
|
|
15
|
+
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
from python_tty.commands.core import BaseCommands, CommandValidator
|
|
2
|
+
from python_tty.commands.registry import (
|
|
3
|
+
ArgSpec,
|
|
4
|
+
CommandDef,
|
|
5
|
+
CommandInfo,
|
|
6
|
+
CommandRegistry,
|
|
7
|
+
CommandStyle,
|
|
8
|
+
COMMAND_REGISTRY,
|
|
9
|
+
define_command_style,
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
__all__ = [
|
|
13
|
+
"ArgSpec",
|
|
14
|
+
"BaseCommands",
|
|
15
|
+
"CommandDef",
|
|
16
|
+
"CommandInfo",
|
|
17
|
+
"CommandRegistry",
|
|
18
|
+
"CommandStyle",
|
|
19
|
+
"COMMAND_REGISTRY",
|
|
20
|
+
"CommandValidator",
|
|
21
|
+
"define_command_style",
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
from prompt_toolkit.completion import NestedCompleter
|
|
2
|
+
from prompt_toolkit.document import Document
|
|
3
|
+
from prompt_toolkit.validation import DummyValidator, Validator, ValidationError
|
|
4
|
+
|
|
5
|
+
from python_tty.commands.registry import COMMAND_REGISTRY, ArgSpec
|
|
6
|
+
from python_tty.exceptions.console_exception import ConsoleInitException
|
|
7
|
+
from python_tty.utils import split_cmd
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class CommandValidator(Validator):
|
|
11
|
+
def __init__(self, command_validators: dict, enable_undefined_command=False):
|
|
12
|
+
self.command_validators = command_validators
|
|
13
|
+
self.enable_undefined_command = enable_undefined_command
|
|
14
|
+
super().__init__()
|
|
15
|
+
|
|
16
|
+
def validate(self, document: Document) -> None:
|
|
17
|
+
try:
|
|
18
|
+
token, arg_text, _ = split_cmd(document.text)
|
|
19
|
+
if token in self.command_validators.keys():
|
|
20
|
+
cmd_validator = self.command_validators[token]
|
|
21
|
+
cmd_validator.validate(Document(text=arg_text))
|
|
22
|
+
else:
|
|
23
|
+
if not self.enable_undefined_command:
|
|
24
|
+
raise ValidationError(message="Bad command")
|
|
25
|
+
except ValueError:
|
|
26
|
+
return
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class BaseCommands:
|
|
30
|
+
def __init__(self, console, registry=None):
|
|
31
|
+
self.console = console
|
|
32
|
+
self.registry = registry if registry is not None else COMMAND_REGISTRY
|
|
33
|
+
self.command_defs = []
|
|
34
|
+
self.command_defs_by_name = {}
|
|
35
|
+
self.command_defs_by_id = {}
|
|
36
|
+
self.command_completers = {}
|
|
37
|
+
self.command_validators = {}
|
|
38
|
+
self.command_funcs = {}
|
|
39
|
+
self._init_funcs()
|
|
40
|
+
self.completer = NestedCompleter.from_nested_dict(self.command_completers)
|
|
41
|
+
self.validator = CommandValidator(self.command_validators, self.enable_undefined_command)
|
|
42
|
+
|
|
43
|
+
@property
|
|
44
|
+
def enable_undefined_command(self):
|
|
45
|
+
return False
|
|
46
|
+
|
|
47
|
+
def _init_funcs(self):
|
|
48
|
+
if self.console is None:
|
|
49
|
+
raise ConsoleInitException("Console is None")
|
|
50
|
+
defs = self.registry.get_command_defs_for_console(self.console.__class__)
|
|
51
|
+
if len(defs) == 0:
|
|
52
|
+
defs = self.registry.collect_from_commands_cls(self.__class__)
|
|
53
|
+
self.command_defs = defs
|
|
54
|
+
self._collect_completer_and_validator(defs)
|
|
55
|
+
|
|
56
|
+
def _collect_completer_and_validator(self, defs):
|
|
57
|
+
for command_def in defs:
|
|
58
|
+
self._map_components(command_def)
|
|
59
|
+
|
|
60
|
+
def _map_components(self, command_def):
|
|
61
|
+
command_id = self._build_command_id(command_def)
|
|
62
|
+
if command_id is not None:
|
|
63
|
+
self.command_defs_by_id[command_id] = command_def
|
|
64
|
+
for command_name in command_def.all_names():
|
|
65
|
+
self.command_funcs[command_name] = command_def.func
|
|
66
|
+
self.command_defs_by_name[command_name] = command_def
|
|
67
|
+
if command_def.completer is None:
|
|
68
|
+
self.command_completers[command_name] = None
|
|
69
|
+
else:
|
|
70
|
+
self.command_completers[command_name] = self._build_completer(command_def)
|
|
71
|
+
self.command_validators[command_name] = self._build_validator(command_def)
|
|
72
|
+
|
|
73
|
+
def _build_completer(self, command_def):
|
|
74
|
+
try:
|
|
75
|
+
return command_def.completer(self.console, command_def.arg_spec)
|
|
76
|
+
except TypeError:
|
|
77
|
+
try:
|
|
78
|
+
return command_def.completer(self.console)
|
|
79
|
+
except TypeError as exc:
|
|
80
|
+
raise ConsoleInitException(
|
|
81
|
+
"Completer init failed. Use completer_from(...) to adapt "
|
|
82
|
+
"prompt_toolkit completers."
|
|
83
|
+
) from exc
|
|
84
|
+
|
|
85
|
+
def _build_validator(self, command_def):
|
|
86
|
+
if command_def.validator is None:
|
|
87
|
+
return DummyValidator()
|
|
88
|
+
try:
|
|
89
|
+
return command_def.validator(self.console, command_def.func, command_def.arg_spec)
|
|
90
|
+
except TypeError:
|
|
91
|
+
return command_def.validator(self.console, command_def.func)
|
|
92
|
+
|
|
93
|
+
def get_command_def(self, command_name):
|
|
94
|
+
command_def = self.command_defs_by_id.get(command_name)
|
|
95
|
+
if command_def is not None:
|
|
96
|
+
return command_def
|
|
97
|
+
return self.command_defs_by_name.get(command_name)
|
|
98
|
+
|
|
99
|
+
def get_command_def_by_id(self, command_id):
|
|
100
|
+
return self.command_defs_by_id.get(command_id)
|
|
101
|
+
|
|
102
|
+
def get_command_id(self, command_name):
|
|
103
|
+
command_def = self.command_defs_by_name.get(command_name)
|
|
104
|
+
if command_def is None:
|
|
105
|
+
return None
|
|
106
|
+
return self._build_command_id(command_def)
|
|
107
|
+
|
|
108
|
+
def _build_command_id(self, command_def):
|
|
109
|
+
console_name = getattr(self.console, "console_name", None)
|
|
110
|
+
if not console_name:
|
|
111
|
+
console_name = self.console.__class__.__name__.lower()
|
|
112
|
+
return f"cmd:{console_name}:{command_def.func_name}"
|
|
113
|
+
|
|
114
|
+
def deserialize_args(self, command_def, raw_text):
|
|
115
|
+
if command_def.arg_spec is None:
|
|
116
|
+
arg_spec = ArgSpec.from_signature(command_def.func)
|
|
117
|
+
return arg_spec.parse(raw_text)
|
|
118
|
+
return command_def.arg_spec.parse(raw_text)
|
|
119
|
+
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
from functools import wraps
|
|
2
|
+
|
|
3
|
+
from prompt_toolkit.completion import Completer
|
|
4
|
+
from prompt_toolkit.validation import Validator
|
|
5
|
+
|
|
6
|
+
from python_tty.commands import BaseCommands
|
|
7
|
+
from python_tty.commands.registry import COMMAND_REGISTRY, CommandInfo, CommandStyle, define_command_style
|
|
8
|
+
from python_tty.exceptions.console_exception import ConsoleInitException
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def commands(commands_cls):
|
|
12
|
+
"""Bind a BaseCommands subclass to a Console class for auto command wiring."""
|
|
13
|
+
if not issubclass(commands_cls, BaseCommands):
|
|
14
|
+
raise ConsoleInitException("Commands must inherit BaseCommands")
|
|
15
|
+
|
|
16
|
+
def decorator(console_cls):
|
|
17
|
+
from python_tty.consoles import MainConsole, SubConsole
|
|
18
|
+
if not issubclass(console_cls, (MainConsole, SubConsole)):
|
|
19
|
+
raise ConsoleInitException("commands decorator must target a Console class")
|
|
20
|
+
existing = getattr(console_cls, "__commands_cls__", None)
|
|
21
|
+
if existing is not None and existing is not commands_cls:
|
|
22
|
+
raise ConsoleInitException(
|
|
23
|
+
f"{console_cls.__name__} already binds to {existing.__name__}; "
|
|
24
|
+
f"cannot bind to {commands_cls.__name__} again"
|
|
25
|
+
)
|
|
26
|
+
setattr(console_cls, "__commands_cls__", commands_cls)
|
|
27
|
+
COMMAND_REGISTRY.register_console_commands(console_cls, commands_cls)
|
|
28
|
+
return console_cls
|
|
29
|
+
|
|
30
|
+
return decorator
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def register_command(command_name: str, command_description: str, command_alias=None,
|
|
34
|
+
command_style=CommandStyle.LOWERCASE,
|
|
35
|
+
completer=None, validator=None, arg_spec=None):
|
|
36
|
+
"""Declare command metadata for a command method on a BaseCommands subclass."""
|
|
37
|
+
if completer is not None and not isinstance(completer, type):
|
|
38
|
+
raise ConsoleInitException("Command completer must be a class")
|
|
39
|
+
if validator is not None and not isinstance(validator, type):
|
|
40
|
+
raise ConsoleInitException("Command validator must be a class")
|
|
41
|
+
if completer is not None and not issubclass(completer, Completer):
|
|
42
|
+
raise ConsoleInitException("Command completer must inherit Completer")
|
|
43
|
+
if validator is not None and not issubclass(validator, Validator):
|
|
44
|
+
raise ConsoleInitException("Command validator must inherit Validator")
|
|
45
|
+
def inner_wrapper(func):
|
|
46
|
+
func.info = CommandInfo(define_command_style(command_name, command_style), command_description,
|
|
47
|
+
completer, validator, command_alias, arg_spec)
|
|
48
|
+
func.type = None
|
|
49
|
+
|
|
50
|
+
@wraps(func)
|
|
51
|
+
def wrapper(*args, **kwargs):
|
|
52
|
+
result = func(*args, **kwargs)
|
|
53
|
+
return result
|
|
54
|
+
|
|
55
|
+
return wrapper
|
|
56
|
+
|
|
57
|
+
return inner_wrapper
|
|
58
|
+
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
from python_tty.commands import BaseCommands
|
|
2
|
+
from python_tty.commands.decorators import register_command
|
|
3
|
+
from python_tty.commands.general import GeneralValidator
|
|
4
|
+
from python_tty.commands.mixins import HelpMixin, QuitMixin
|
|
5
|
+
from python_tty.ui.events import UIEventLevel
|
|
6
|
+
from python_tty.ui.output import proxy_print
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class RootCommands(BaseCommands, HelpMixin, QuitMixin):
|
|
10
|
+
@property
|
|
11
|
+
def enable_undefined_command(self):
|
|
12
|
+
return True
|
|
13
|
+
|
|
14
|
+
@register_command("use", "Enter sub console", validator=GeneralValidator)
|
|
15
|
+
def run_use(self, console_name):
|
|
16
|
+
manager = getattr(self.console, "manager", None)
|
|
17
|
+
if manager is None:
|
|
18
|
+
proxy_print("Console manager not configured", UIEventLevel.WARNING)
|
|
19
|
+
return
|
|
20
|
+
if not manager.is_registered(console_name):
|
|
21
|
+
proxy_print(f"Console [{console_name}] not registered", UIEventLevel.ERROR)
|
|
22
|
+
return
|
|
23
|
+
manager.push(console_name)
|
|
24
|
+
|
|
25
|
+
@register_command("debug", "Debug root console, display some information", validator=GeneralValidator)
|
|
26
|
+
def run_debug(self, *args):
|
|
27
|
+
framework = self.console.service
|
|
28
|
+
proxy_print(str(framework))
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
if __name__ == '__main__':
|
|
32
|
+
pass
|
|
33
|
+
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
from python_tty.commands import BaseCommands
|
|
2
|
+
from python_tty.commands.decorators import register_command
|
|
3
|
+
from python_tty.commands.general import GeneralValidator
|
|
4
|
+
from python_tty.commands.mixins import BackMixin, HelpMixin, QuitMixin
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class SubCommands(BaseCommands, HelpMixin, QuitMixin, BackMixin):
|
|
8
|
+
@register_command("debug", "Debug command, display some information", [], validator=GeneralValidator)
|
|
9
|
+
def run_debug(self):
|
|
10
|
+
pass
|
|
11
|
+
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
from abc import ABC, abstractmethod
|
|
2
|
+
|
|
3
|
+
from prompt_toolkit.completion import Completer, WordCompleter
|
|
4
|
+
from prompt_toolkit.document import Document
|
|
5
|
+
from prompt_toolkit.validation import ValidationError, Validator
|
|
6
|
+
|
|
7
|
+
from python_tty.commands.registry import ArgSpec
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class GeneralValidator(Validator):
|
|
11
|
+
"""Default validator that checks argument count and allows custom validation."""
|
|
12
|
+
def __init__(self, console, func, arg_spec=None):
|
|
13
|
+
self.console = console
|
|
14
|
+
self.func = func
|
|
15
|
+
self.arg_spec = arg_spec or ArgSpec.from_signature(func)
|
|
16
|
+
super().__init__()
|
|
17
|
+
|
|
18
|
+
def validate(self, document: Document) -> None:
|
|
19
|
+
try:
|
|
20
|
+
args = self.arg_spec.parse(document.text)
|
|
21
|
+
self.arg_spec.validate_count(len(args))
|
|
22
|
+
except ValidationError:
|
|
23
|
+
raise
|
|
24
|
+
except ValueError as exc:
|
|
25
|
+
raise ValidationError(message=str(exc)) from exc
|
|
26
|
+
try:
|
|
27
|
+
self.custom_validate(args, document.text)
|
|
28
|
+
except TypeError:
|
|
29
|
+
self.custom_validate(document.text)
|
|
30
|
+
|
|
31
|
+
def custom_validate(self, args, text: str):
|
|
32
|
+
pass
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _allow_complete_for_spec(arg_spec, text, args):
|
|
36
|
+
if arg_spec.max_args is None:
|
|
37
|
+
return True
|
|
38
|
+
if text != "" and text[-1].isspace():
|
|
39
|
+
return len(args) < arg_spec.max_args
|
|
40
|
+
return len(args) <= arg_spec.max_args
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class GeneralCompleter(Completer, ABC):
|
|
44
|
+
"""Base completer with ArgSpec-aware completion and console injection."""
|
|
45
|
+
def __init__(self, console, arg_spec=None, ignore_case=True):
|
|
46
|
+
self.console = console
|
|
47
|
+
self.arg_spec = arg_spec or ArgSpec()
|
|
48
|
+
self.ignore_case = ignore_case
|
|
49
|
+
super().__init__()
|
|
50
|
+
|
|
51
|
+
@abstractmethod
|
|
52
|
+
def get_candidates(self, args, text: str):
|
|
53
|
+
pass
|
|
54
|
+
|
|
55
|
+
def get_completions(self, document, complete_event):
|
|
56
|
+
text = document.text_before_cursor
|
|
57
|
+
try:
|
|
58
|
+
args = self.arg_spec.parse(text)
|
|
59
|
+
except ValueError:
|
|
60
|
+
return
|
|
61
|
+
if not _allow_complete_for_spec(self.arg_spec, text, args):
|
|
62
|
+
return
|
|
63
|
+
words = self.get_candidates(args, text)
|
|
64
|
+
if not words:
|
|
65
|
+
return
|
|
66
|
+
completer = WordCompleter(words, ignore_case=self.ignore_case)
|
|
67
|
+
yield from completer.get_completions(document, complete_event)
|
|
68
|
+
|
|
69
|
+
def _allow_complete(self, text, args):
|
|
70
|
+
return _allow_complete_for_spec(self.arg_spec, text, args)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
class PromptToolkitCompleterAdapter(Completer):
|
|
74
|
+
completer_cls = None
|
|
75
|
+
completer_kwargs = {}
|
|
76
|
+
|
|
77
|
+
def __init__(self, console, arg_spec=None):
|
|
78
|
+
self.console = console
|
|
79
|
+
self.arg_spec = arg_spec or ArgSpec()
|
|
80
|
+
if self.completer_cls is None:
|
|
81
|
+
raise ValueError("completer_cls must be set for adapter")
|
|
82
|
+
self._inner = self.completer_cls(**self.get_completer_kwargs())
|
|
83
|
+
super().__init__()
|
|
84
|
+
|
|
85
|
+
def get_completer_kwargs(self):
|
|
86
|
+
return dict(self.completer_kwargs)
|
|
87
|
+
|
|
88
|
+
def get_completions(self, document, complete_event):
|
|
89
|
+
text = document.text_before_cursor
|
|
90
|
+
try:
|
|
91
|
+
args = self.arg_spec.parse(text)
|
|
92
|
+
except ValueError:
|
|
93
|
+
return
|
|
94
|
+
if not _allow_complete_for_spec(self.arg_spec, text, args):
|
|
95
|
+
return
|
|
96
|
+
yield from self._inner.get_completions(document, complete_event)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def completer_from(completer_cls, **kwargs):
|
|
100
|
+
"""Build a completer adapter class for a prompt_toolkit completer."""
|
|
101
|
+
class _Adapter(PromptToolkitCompleterAdapter):
|
|
102
|
+
pass
|
|
103
|
+
|
|
104
|
+
_Adapter.completer_cls = completer_cls
|
|
105
|
+
_Adapter.completer_kwargs = kwargs
|
|
106
|
+
return _Adapter
|
|
107
|
+
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import inspect
|
|
2
|
+
|
|
3
|
+
from python_tty.ui.output import proxy_print
|
|
4
|
+
from python_tty.commands import BaseCommands
|
|
5
|
+
from python_tty.commands.decorators import register_command
|
|
6
|
+
from python_tty.commands.general import GeneralValidator
|
|
7
|
+
from python_tty.exceptions.console_exception import ConsoleExit, SubConsoleExit
|
|
8
|
+
from python_tty.utils.table import Table
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class CommandMixin:
|
|
12
|
+
pass
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class BackMixin(CommandMixin):
|
|
16
|
+
@register_command("back", "Back to forward tty", validator=GeneralValidator)
|
|
17
|
+
def run_back(self):
|
|
18
|
+
raise SubConsoleExit
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class QuitMixin(CommandMixin):
|
|
22
|
+
@register_command("quit", "Quit Console", ["exit", "q"], validator=GeneralValidator)
|
|
23
|
+
def run_quit(self):
|
|
24
|
+
raise ConsoleExit
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class HelpMixin(CommandMixin):
|
|
28
|
+
@register_command("help", "Display help information", ["?"], validator=GeneralValidator)
|
|
29
|
+
def run_help(self):
|
|
30
|
+
header = ["Command", "Description"]
|
|
31
|
+
base_funcs = []
|
|
32
|
+
custom_funcs = []
|
|
33
|
+
base_commands_funcs = []
|
|
34
|
+
for cls in self.__class__.mro():
|
|
35
|
+
if cls is CommandMixin:
|
|
36
|
+
continue
|
|
37
|
+
if issubclass(cls, CommandMixin):
|
|
38
|
+
base_commands_funcs.extend([member[1] for member in inspect.getmembers(cls, inspect.isfunction)])
|
|
39
|
+
for name, func in self.command_funcs.items():
|
|
40
|
+
row = [name, func.info.func_description]
|
|
41
|
+
if func in base_commands_funcs:
|
|
42
|
+
base_funcs.append(row)
|
|
43
|
+
else:
|
|
44
|
+
custom_funcs.append(row)
|
|
45
|
+
if base_funcs:
|
|
46
|
+
proxy_print(Table(header, base_funcs, "Core Commands"))
|
|
47
|
+
if custom_funcs:
|
|
48
|
+
proxy_print(Table(header, custom_funcs, "Custom Commands"))
|
|
49
|
+
|
|
50
|
+
class DefaultCommands(BaseCommands, HelpMixin, QuitMixin):
|
|
51
|
+
pass
|
|
52
|
+
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import re
|
|
2
|
+
import enum
|
|
3
|
+
import inspect
|
|
4
|
+
|
|
5
|
+
from prompt_toolkit.completion import Completer
|
|
6
|
+
from prompt_toolkit.validation import ValidationError, Validator
|
|
7
|
+
from python_tty.exceptions.console_exception import ConsoleInitException
|
|
8
|
+
from python_tty.utils.tokenize import tokenize_cmd
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def define_command_style(command_name, style):
|
|
13
|
+
if style == CommandStyle.NONE:
|
|
14
|
+
return command_name
|
|
15
|
+
elif style == CommandStyle.LOWERCASE:
|
|
16
|
+
return command_name.lower()
|
|
17
|
+
elif style == CommandStyle.UPPERCASE:
|
|
18
|
+
return command_name.upper()
|
|
19
|
+
command_name = re.sub(r'(.)([A-Z][a-z]+)', r'\1-\2', command_name)
|
|
20
|
+
command_name = re.sub(r'([a-z0-9])([A-Z])', r'\1-\2', command_name)
|
|
21
|
+
if style == CommandStyle.POWERSHELL:
|
|
22
|
+
return command_name
|
|
23
|
+
elif style == CommandStyle.SLUGIFIED:
|
|
24
|
+
return command_name.lower()
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class CommandStyle(enum.Enum):
|
|
28
|
+
NONE = 0 # ClassName => ClassName
|
|
29
|
+
LOWERCASE = 1 # ClassName => classname
|
|
30
|
+
UPPERCASE = 2 # ClassName => CLASSNAME
|
|
31
|
+
POWERSHELL = 3 # ClassName => Class-Name
|
|
32
|
+
SLUGIFIED = 4 # ClassName => class-name
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class ArgSpec:
|
|
36
|
+
def __init__(self, min_args=0, max_args=0, variadic=False):
|
|
37
|
+
self.min_args = min_args
|
|
38
|
+
self.max_args = max_args
|
|
39
|
+
self.variadic = variadic
|
|
40
|
+
|
|
41
|
+
@classmethod
|
|
42
|
+
def from_signature(cls, func, skip_first=True):
|
|
43
|
+
sig = inspect.signature(func)
|
|
44
|
+
params = list(sig.parameters.values())
|
|
45
|
+
if skip_first and params:
|
|
46
|
+
params = params[1:]
|
|
47
|
+
min_args = 0
|
|
48
|
+
max_args = 0
|
|
49
|
+
variadic = False
|
|
50
|
+
for param in params:
|
|
51
|
+
if param.kind == param.VAR_POSITIONAL:
|
|
52
|
+
variadic = True
|
|
53
|
+
continue
|
|
54
|
+
if param.default is param.empty:
|
|
55
|
+
min_args += 1
|
|
56
|
+
max_args += 1
|
|
57
|
+
if variadic:
|
|
58
|
+
max_args = None
|
|
59
|
+
return cls(min_args, max_args, variadic)
|
|
60
|
+
|
|
61
|
+
def parse(self, text):
|
|
62
|
+
tokens = tokenize_cmd(text)
|
|
63
|
+
return tokens
|
|
64
|
+
|
|
65
|
+
def count_args(self, text):
|
|
66
|
+
tokens = tokenize_cmd(text)
|
|
67
|
+
return len(tokens)
|
|
68
|
+
|
|
69
|
+
def validate_count(self, count):
|
|
70
|
+
if count < self.min_args:
|
|
71
|
+
raise ValidationError(message="Not enough parameters set!")
|
|
72
|
+
if self.max_args is not None and count > self.max_args:
|
|
73
|
+
raise ValidationError(message="Too many parameters set!")
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
class CommandInfo:
|
|
77
|
+
def __init__(self, func_name, func_description,
|
|
78
|
+
completer=None, validator=None,
|
|
79
|
+
command_alias=None, arg_spec=None):
|
|
80
|
+
self.func_name = func_name
|
|
81
|
+
self.func_description = func_description
|
|
82
|
+
self.completer = completer
|
|
83
|
+
self.validator = validator
|
|
84
|
+
self.arg_spec = arg_spec
|
|
85
|
+
if command_alias is None:
|
|
86
|
+
self.alias = []
|
|
87
|
+
else:
|
|
88
|
+
if type(command_alias) == str:
|
|
89
|
+
self.alias = [command_alias]
|
|
90
|
+
elif type(command_alias) == list:
|
|
91
|
+
self.alias = command_alias
|
|
92
|
+
else:
|
|
93
|
+
self.alias = []
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
class CommandDef:
|
|
97
|
+
def __init__(self, func_name, func, func_description,
|
|
98
|
+
command_alias=None, completer=None, validator=None,
|
|
99
|
+
arg_spec=None):
|
|
100
|
+
self.func_name = func_name
|
|
101
|
+
self.func = func
|
|
102
|
+
self.func_description = func_description
|
|
103
|
+
self.completer = completer
|
|
104
|
+
self.validator = validator
|
|
105
|
+
self.arg_spec = arg_spec
|
|
106
|
+
if command_alias is None:
|
|
107
|
+
self.alias = []
|
|
108
|
+
else:
|
|
109
|
+
self.alias = command_alias
|
|
110
|
+
|
|
111
|
+
def all_names(self):
|
|
112
|
+
return [self.func_name] + list(self.alias)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
class CommandRegistry:
|
|
116
|
+
def __init__(self):
|
|
117
|
+
self._console_command_classes = {}
|
|
118
|
+
self._commands_defs = {}
|
|
119
|
+
self._console_defs = {}
|
|
120
|
+
|
|
121
|
+
def register_console_commands(self, console_cls, commands_cls):
|
|
122
|
+
self._console_command_classes[console_cls] = commands_cls
|
|
123
|
+
|
|
124
|
+
def get_commands_cls(self, console_cls):
|
|
125
|
+
return self._console_command_classes.get(console_cls)
|
|
126
|
+
|
|
127
|
+
def register(self, func, console_cls=None, commands_cls=None,
|
|
128
|
+
command_name=None, command_description="", command_alias=None,
|
|
129
|
+
command_style=CommandStyle.LOWERCASE,
|
|
130
|
+
completer=None, validator=None, arg_spec=None):
|
|
131
|
+
if completer is not None and not isinstance(completer, type):
|
|
132
|
+
raise ConsoleInitException("Command completer must be a class")
|
|
133
|
+
if validator is not None and not isinstance(validator, type):
|
|
134
|
+
raise ConsoleInitException("Command validator must be a class")
|
|
135
|
+
if completer is not None and not issubclass(completer, Completer):
|
|
136
|
+
raise ConsoleInitException("Command completer must inherit Completer")
|
|
137
|
+
if validator is not None and not issubclass(validator, Validator):
|
|
138
|
+
raise ConsoleInitException("Command validator must inherit Validator")
|
|
139
|
+
if command_name is None:
|
|
140
|
+
command_name = func.__name__
|
|
141
|
+
info = CommandInfo(define_command_style(command_name, command_style),
|
|
142
|
+
command_description, completer, validator,
|
|
143
|
+
command_alias, arg_spec)
|
|
144
|
+
func.info = info
|
|
145
|
+
func.type = None
|
|
146
|
+
command_def = CommandDef(info.func_name, func, info.func_description,
|
|
147
|
+
info.alias, info.completer, info.validator,
|
|
148
|
+
info.arg_spec)
|
|
149
|
+
if commands_cls is not None:
|
|
150
|
+
self._commands_defs.setdefault(commands_cls, []).append(command_def)
|
|
151
|
+
if console_cls is not None:
|
|
152
|
+
self._console_defs.setdefault(console_cls, []).append(command_def)
|
|
153
|
+
return command_def
|
|
154
|
+
|
|
155
|
+
def collect_from_commands_cls(self, commands_cls):
|
|
156
|
+
if commands_cls in self._commands_defs:
|
|
157
|
+
return self._commands_defs[commands_cls]
|
|
158
|
+
defs = []
|
|
159
|
+
for member_name in dir(commands_cls):
|
|
160
|
+
if member_name.startswith("_"):
|
|
161
|
+
continue
|
|
162
|
+
member = getattr(commands_cls, member_name)
|
|
163
|
+
if (inspect.ismethod(member) or inspect.isfunction(member)) and hasattr(member, "info"):
|
|
164
|
+
command_info = member.info
|
|
165
|
+
arg_spec = command_info.arg_spec or ArgSpec.from_signature(member)
|
|
166
|
+
defs.append(CommandDef(command_info.func_name, member,
|
|
167
|
+
command_info.func_description,
|
|
168
|
+
command_info.alias,
|
|
169
|
+
command_info.completer,
|
|
170
|
+
command_info.validator,
|
|
171
|
+
arg_spec))
|
|
172
|
+
self._commands_defs[commands_cls] = defs
|
|
173
|
+
return defs
|
|
174
|
+
|
|
175
|
+
def get_command_defs_for_console(self, console_cls):
|
|
176
|
+
defs = []
|
|
177
|
+
commands_cls = self.get_commands_cls(console_cls)
|
|
178
|
+
if commands_cls is not None:
|
|
179
|
+
defs.extend(self.collect_from_commands_cls(commands_cls))
|
|
180
|
+
defs.extend(self._console_defs.get(console_cls, []))
|
|
181
|
+
return defs
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
COMMAND_REGISTRY = CommandRegistry()
|
|
185
|
+
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
from dataclasses import dataclass, field
|
|
2
|
+
from typing import Optional, TYPE_CHECKING
|
|
3
|
+
|
|
4
|
+
if TYPE_CHECKING:
|
|
5
|
+
from python_tty.ui.output import OutputRouter
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@dataclass
|
|
9
|
+
class ExecutorConfig:
|
|
10
|
+
workers: int = 1
|
|
11
|
+
retain_last_n: Optional[int] = None
|
|
12
|
+
ttl_seconds: Optional[float] = None
|
|
13
|
+
pop_on_wait: bool = False
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass
|
|
17
|
+
class ConsoleManagerConfig:
|
|
18
|
+
use_patch_stdout: bool = True
|
|
19
|
+
output_router: Optional["OutputRouter"] = None
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@dataclass
|
|
23
|
+
class ConsoleFactoryConfig:
|
|
24
|
+
start_executor: bool = True
|
|
25
|
+
executor_in_thread: bool = True
|
|
26
|
+
executor_thread_name: str = "ExecutorLoop"
|
|
27
|
+
shutdown_executor: bool = True
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclass
|
|
31
|
+
class Config:
|
|
32
|
+
console_manager: ConsoleManagerConfig = field(default_factory=ConsoleManagerConfig)
|
|
33
|
+
executor: ExecutorConfig = field(default_factory=ExecutorConfig)
|
|
34
|
+
console_factory: ConsoleFactoryConfig = field(default_factory=ConsoleFactoryConfig)
|
|
35
|
+
|