python-tty 0.1.0__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.
- python_tty-0.1.0/PKG-INFO +66 -0
- python_tty-0.1.0/README.md +48 -0
- python_tty-0.1.0/pyproject.toml +3 -0
- python_tty-0.1.0/setup.cfg +4 -0
- python_tty-0.1.0/setup.py +23 -0
- python_tty-0.1.0/src/python_tty/__init__.py +15 -0
- python_tty-0.1.0/src/python_tty/commands/__init__.py +24 -0
- python_tty-0.1.0/src/python_tty/commands/core.py +119 -0
- python_tty-0.1.0/src/python_tty/commands/decorators.py +58 -0
- python_tty-0.1.0/src/python_tty/commands/examples/__init__.py +8 -0
- python_tty-0.1.0/src/python_tty/commands/examples/root_commands.py +33 -0
- python_tty-0.1.0/src/python_tty/commands/examples/sub_commands.py +11 -0
- python_tty-0.1.0/src/python_tty/commands/general.py +107 -0
- python_tty-0.1.0/src/python_tty/commands/mixins.py +52 -0
- python_tty-0.1.0/src/python_tty/commands/registry.py +185 -0
- python_tty-0.1.0/src/python_tty/config/__init__.py +9 -0
- python_tty-0.1.0/src/python_tty/config/config.py +35 -0
- python_tty-0.1.0/src/python_tty/console_factory.py +100 -0
- python_tty-0.1.0/src/python_tty/consoles/__init__.py +16 -0
- python_tty-0.1.0/src/python_tty/consoles/core.py +140 -0
- python_tty-0.1.0/src/python_tty/consoles/decorators.py +42 -0
- python_tty-0.1.0/src/python_tty/consoles/examples/__init__.py +8 -0
- python_tty-0.1.0/src/python_tty/consoles/examples/root_console.py +34 -0
- python_tty-0.1.0/src/python_tty/consoles/examples/sub_console.py +34 -0
- python_tty-0.1.0/src/python_tty/consoles/loader.py +14 -0
- python_tty-0.1.0/src/python_tty/consoles/manager.py +146 -0
- python_tty-0.1.0/src/python_tty/consoles/registry.py +102 -0
- python_tty-0.1.0/src/python_tty/exceptions/__init__.py +7 -0
- python_tty-0.1.0/src/python_tty/exceptions/console_exception.py +12 -0
- python_tty-0.1.0/src/python_tty/executor/__init__.py +10 -0
- python_tty-0.1.0/src/python_tty/executor/executor.py +335 -0
- python_tty-0.1.0/src/python_tty/executor/models.py +38 -0
- python_tty-0.1.0/src/python_tty/frontends/__init__.py +0 -0
- python_tty-0.1.0/src/python_tty/meta/__init__.py +0 -0
- python_tty-0.1.0/src/python_tty/ui/__init__.py +13 -0
- python_tty-0.1.0/src/python_tty/ui/events.py +55 -0
- python_tty-0.1.0/src/python_tty/ui/output.py +102 -0
- python_tty-0.1.0/src/python_tty/utils/__init__.py +13 -0
- python_tty-0.1.0/src/python_tty/utils/table.py +126 -0
- python_tty-0.1.0/src/python_tty/utils/tokenize.py +45 -0
- python_tty-0.1.0/src/python_tty/utils/ui_logger.py +17 -0
- python_tty-0.1.0/src/python_tty.egg-info/PKG-INFO +66 -0
- python_tty-0.1.0/src/python_tty.egg-info/SOURCES.txt +44 -0
- python_tty-0.1.0/src/python_tty.egg-info/dependency_links.txt +1 -0
- python_tty-0.1.0/src/python_tty.egg-info/requires.txt +2 -0
- python_tty-0.1.0/src/python_tty.egg-info/top_level.txt +1 -0
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: python-tty
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A multi-console TTY framework for complex CLI/TTY apps
|
|
5
|
+
Home-page: https://github.com/ROOKIEMIE/python-tty
|
|
6
|
+
Author: ROOKIEMIE
|
|
7
|
+
Requires-Python: >=3.10
|
|
8
|
+
Description-Content-Type: text/markdown
|
|
9
|
+
Requires-Dist: prompt_toolkit>=3.0.32
|
|
10
|
+
Requires-Dist: tqdm
|
|
11
|
+
Dynamic: author
|
|
12
|
+
Dynamic: description
|
|
13
|
+
Dynamic: description-content-type
|
|
14
|
+
Dynamic: home-page
|
|
15
|
+
Dynamic: requires-dist
|
|
16
|
+
Dynamic: requires-python
|
|
17
|
+
Dynamic: summary
|
|
18
|
+
|
|
19
|
+
# Command Line Framework (V2: Web/RPC-ready)
|
|
20
|
+
|
|
21
|
+
[中文](README_zh.md)
|
|
22
|
+
|
|
23
|
+
V2 extends the TTY core toward Web + RPC, while keeping the current codebase focused on the TTY runtime. At the moment, Web/RPC is planned but not yet implemented.
|
|
24
|
+
|
|
25
|
+
## Quick Start (TTY Core)
|
|
26
|
+
|
|
27
|
+
1) Define your consoles and commands (see examples in `src/consoles/examples` and `src/commands/examples`).
|
|
28
|
+
2) Ensure console modules are imported so decorators can register them:
|
|
29
|
+
- Update `DEFAULT_CONSOLE_MODULES` in `src/consoles/loader.py`, or
|
|
30
|
+
- Call `load_consoles([...])` manually.
|
|
31
|
+
3) Start the factory:
|
|
32
|
+
|
|
33
|
+
```python
|
|
34
|
+
from src.console_factory import ConsoleFactory
|
|
35
|
+
|
|
36
|
+
factory = ConsoleFactory(service=my_business_core)
|
|
37
|
+
factory.start()
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
The `service` instance is available in all consoles via `console.service`, and in commands via `self.console.service`. If `service` does not inherit `UIEventSpeaker`, a warning is emitted at startup.
|
|
41
|
+
|
|
42
|
+
## Current Capabilities (TTY Core)
|
|
43
|
+
|
|
44
|
+
Console layer:
|
|
45
|
+
- `src/consoles/core.py`: `BaseConsole`, `MainConsole`, `SubConsole`
|
|
46
|
+
- `src/consoles/manager.py` and `src/consoles/registry.py` for lifecycle and registration
|
|
47
|
+
|
|
48
|
+
Commands layer:
|
|
49
|
+
- `src/commands/core.py`: `BaseCommands`, `CommandValidator`
|
|
50
|
+
- `src/commands/registry.py`: `CommandRegistry`, `ArgSpec`
|
|
51
|
+
- `src/commands/general.py`: `GeneralValidator`, `GeneralCompleter`
|
|
52
|
+
- `src/commands/mixins.py`: `CommandMixin` and built-in mixins
|
|
53
|
+
|
|
54
|
+
UI and utilities:
|
|
55
|
+
- `src/core/events.py`: `UIEvent`, `UIEventLevel`, `UIEventSpeaker`
|
|
56
|
+
- `src/ui/output.py`: `proxy_print`
|
|
57
|
+
- `src/utils/`: `tokenize.py`, `table.py`, `ui_logger.py`
|
|
58
|
+
|
|
59
|
+
## Roadmap (V2)
|
|
60
|
+
|
|
61
|
+
Planned milestones for Web + RPC:
|
|
62
|
+
- M1: Meta Descriptor v1 + Exporter
|
|
63
|
+
- M2: RPC proto v1 + local (de)serialization
|
|
64
|
+
- M3: Meta Web Server (HTTP + WS)
|
|
65
|
+
- M4: RPC Server (mTLS + allowlist + audit)
|
|
66
|
+
- M5: Unified execution system (CommandExecutor)
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# Command Line Framework (V2: Web/RPC-ready)
|
|
2
|
+
|
|
3
|
+
[中文](README_zh.md)
|
|
4
|
+
|
|
5
|
+
V2 extends the TTY core toward Web + RPC, while keeping the current codebase focused on the TTY runtime. At the moment, Web/RPC is planned but not yet implemented.
|
|
6
|
+
|
|
7
|
+
## Quick Start (TTY Core)
|
|
8
|
+
|
|
9
|
+
1) Define your consoles and commands (see examples in `src/consoles/examples` and `src/commands/examples`).
|
|
10
|
+
2) Ensure console modules are imported so decorators can register them:
|
|
11
|
+
- Update `DEFAULT_CONSOLE_MODULES` in `src/consoles/loader.py`, or
|
|
12
|
+
- Call `load_consoles([...])` manually.
|
|
13
|
+
3) Start the factory:
|
|
14
|
+
|
|
15
|
+
```python
|
|
16
|
+
from src.console_factory import ConsoleFactory
|
|
17
|
+
|
|
18
|
+
factory = ConsoleFactory(service=my_business_core)
|
|
19
|
+
factory.start()
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
The `service` instance is available in all consoles via `console.service`, and in commands via `self.console.service`. If `service` does not inherit `UIEventSpeaker`, a warning is emitted at startup.
|
|
23
|
+
|
|
24
|
+
## Current Capabilities (TTY Core)
|
|
25
|
+
|
|
26
|
+
Console layer:
|
|
27
|
+
- `src/consoles/core.py`: `BaseConsole`, `MainConsole`, `SubConsole`
|
|
28
|
+
- `src/consoles/manager.py` and `src/consoles/registry.py` for lifecycle and registration
|
|
29
|
+
|
|
30
|
+
Commands layer:
|
|
31
|
+
- `src/commands/core.py`: `BaseCommands`, `CommandValidator`
|
|
32
|
+
- `src/commands/registry.py`: `CommandRegistry`, `ArgSpec`
|
|
33
|
+
- `src/commands/general.py`: `GeneralValidator`, `GeneralCompleter`
|
|
34
|
+
- `src/commands/mixins.py`: `CommandMixin` and built-in mixins
|
|
35
|
+
|
|
36
|
+
UI and utilities:
|
|
37
|
+
- `src/core/events.py`: `UIEvent`, `UIEventLevel`, `UIEventSpeaker`
|
|
38
|
+
- `src/ui/output.py`: `proxy_print`
|
|
39
|
+
- `src/utils/`: `tokenize.py`, `table.py`, `ui_logger.py`
|
|
40
|
+
|
|
41
|
+
## Roadmap (V2)
|
|
42
|
+
|
|
43
|
+
Planned milestones for Web + RPC:
|
|
44
|
+
- M1: Meta Descriptor v1 + Exporter
|
|
45
|
+
- M2: RPC proto v1 + local (de)serialization
|
|
46
|
+
- M3: Meta Web Server (HTTP + WS)
|
|
47
|
+
- M4: RPC Server (mTLS + allowlist + audit)
|
|
48
|
+
- M5: Unified execution system (CommandExecutor)
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
from setuptools import setup, find_packages
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
ROOT = Path(__file__).parent
|
|
5
|
+
long_description = (ROOT / "README.md").read_text(encoding="utf-8")
|
|
6
|
+
|
|
7
|
+
setup(
|
|
8
|
+
name="python-tty", # PyPI 分发名(可以有连字符)
|
|
9
|
+
version="0.1.0", # 先用手动版本;后续可切 tag 自动版本
|
|
10
|
+
description="A multi-console TTY framework for complex CLI/TTY apps",
|
|
11
|
+
long_description=long_description,
|
|
12
|
+
long_description_content_type="text/markdown",
|
|
13
|
+
author="ROOKIEMIE",
|
|
14
|
+
url="https://github.com/ROOKIEMIE/python-tty",
|
|
15
|
+
package_dir={"": "src"},
|
|
16
|
+
packages=find_packages(where="src", exclude=("tests*", "demos*", "docs*")),
|
|
17
|
+
include_package_data=True,
|
|
18
|
+
python_requires=">=3.10",
|
|
19
|
+
install_requires=[
|
|
20
|
+
"prompt_toolkit>=3.0.32",
|
|
21
|
+
"tqdm",
|
|
22
|
+
],
|
|
23
|
+
)
|
|
@@ -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
|
+
|