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
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import threading
|
|
3
|
+
|
|
4
|
+
from python_tty.config import Config
|
|
5
|
+
from python_tty.consoles.loader import load_consoles
|
|
6
|
+
from python_tty.consoles.manager import ConsoleManager
|
|
7
|
+
from python_tty.consoles.registry import REGISTRY
|
|
8
|
+
from python_tty.executor import CommandExecutor
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class ConsoleFactory:
|
|
12
|
+
"""Bootstrap console system by loading modules and registering consoles.
|
|
13
|
+
|
|
14
|
+
Example:
|
|
15
|
+
# Pass your business core instance here to make it available
|
|
16
|
+
# to all console/commands classes via the manager service.
|
|
17
|
+
factory = ConsoleFactory(object())
|
|
18
|
+
factory.start()
|
|
19
|
+
|
|
20
|
+
Notes:
|
|
21
|
+
- To auto-load consoles, update DEFAULT_CONSOLE_MODULES in
|
|
22
|
+
python_tty.consoles.loader with the modules that define your console classes.
|
|
23
|
+
- Or call load_consoles(...) yourself before starting to register
|
|
24
|
+
consoles via their decorators.
|
|
25
|
+
"""
|
|
26
|
+
def __init__(self, service=None, config: Config = None):
|
|
27
|
+
if config is None:
|
|
28
|
+
config = Config()
|
|
29
|
+
self.config = config
|
|
30
|
+
self.executor = CommandExecutor(config=config.executor)
|
|
31
|
+
self._executor_loop = None
|
|
32
|
+
self._executor_thread = None
|
|
33
|
+
self.manager = ConsoleManager(
|
|
34
|
+
service=service,
|
|
35
|
+
executor=self.executor,
|
|
36
|
+
on_shutdown=self.shutdown,
|
|
37
|
+
config=config.console_manager,
|
|
38
|
+
)
|
|
39
|
+
load_consoles()
|
|
40
|
+
REGISTRY.register_all(self.manager)
|
|
41
|
+
|
|
42
|
+
def start(self):
|
|
43
|
+
"""Start the console loop with the registered root console."""
|
|
44
|
+
self._start_executor_if_needed()
|
|
45
|
+
self.manager.run()
|
|
46
|
+
|
|
47
|
+
def start_executor(self, loop=None):
|
|
48
|
+
"""Start executor workers on the provided asyncio loop."""
|
|
49
|
+
self.executor.start(loop=loop)
|
|
50
|
+
|
|
51
|
+
def shutdown_executor(self, wait=True, timeout=None):
|
|
52
|
+
"""Shutdown executor workers after RPC/TTY stop."""
|
|
53
|
+
loop = self._executor_loop
|
|
54
|
+
if loop is not None and loop.is_running():
|
|
55
|
+
self.executor.shutdown_threadsafe(wait=wait, timeout=timeout)
|
|
56
|
+
loop.call_soon_threadsafe(loop.stop)
|
|
57
|
+
if self._executor_thread is not None and wait:
|
|
58
|
+
self._executor_thread.join(timeout)
|
|
59
|
+
return None
|
|
60
|
+
return self.executor.shutdown_threadsafe(wait=wait, timeout=timeout)
|
|
61
|
+
|
|
62
|
+
def shutdown(self):
|
|
63
|
+
"""Shutdown all resources owned by the factory."""
|
|
64
|
+
if self.config.console_factory.shutdown_executor:
|
|
65
|
+
self.shutdown_executor()
|
|
66
|
+
|
|
67
|
+
def _start_executor_if_needed(self):
|
|
68
|
+
if not self.config.console_factory.start_executor:
|
|
69
|
+
return
|
|
70
|
+
if self._executor_thread is not None and self._executor_thread.is_alive():
|
|
71
|
+
return
|
|
72
|
+
if self.config.console_factory.executor_in_thread:
|
|
73
|
+
self._start_executor_thread()
|
|
74
|
+
else:
|
|
75
|
+
self.start_executor()
|
|
76
|
+
|
|
77
|
+
def _start_executor_thread(self):
|
|
78
|
+
if self._executor_thread is not None and self._executor_thread.is_alive():
|
|
79
|
+
return
|
|
80
|
+
loop = asyncio.new_event_loop()
|
|
81
|
+
self._executor_loop = loop
|
|
82
|
+
|
|
83
|
+
def _run_loop():
|
|
84
|
+
asyncio.set_event_loop(loop)
|
|
85
|
+
self.start_executor(loop=loop)
|
|
86
|
+
loop.run_forever()
|
|
87
|
+
pending = asyncio.all_tasks(loop)
|
|
88
|
+
if pending:
|
|
89
|
+
for task in pending:
|
|
90
|
+
task.cancel()
|
|
91
|
+
loop.run_until_complete(asyncio.gather(*pending, return_exceptions=True))
|
|
92
|
+
loop.close()
|
|
93
|
+
|
|
94
|
+
self._executor_thread = threading.Thread(
|
|
95
|
+
target=_run_loop,
|
|
96
|
+
name=self.config.console_factory.executor_thread_name,
|
|
97
|
+
daemon=True,
|
|
98
|
+
)
|
|
99
|
+
self._executor_thread.start()
|
|
100
|
+
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
from python_tty.consoles.core import BaseConsole, MainConsole, SubConsole
|
|
2
|
+
from python_tty.consoles.decorators import root, sub, multi
|
|
3
|
+
from python_tty.consoles.registry import REGISTRY
|
|
4
|
+
from python_tty.consoles.loader import DEFAULT_CONSOLE_MODULES
|
|
5
|
+
|
|
6
|
+
__all__ = [
|
|
7
|
+
"BaseConsole",
|
|
8
|
+
"MainConsole",
|
|
9
|
+
"SubConsole",
|
|
10
|
+
"DEFAULT_CONSOLE_MODULES",
|
|
11
|
+
"REGISTRY",
|
|
12
|
+
"root",
|
|
13
|
+
"sub",
|
|
14
|
+
"multi",
|
|
15
|
+
]
|
|
16
|
+
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import uuid
|
|
2
|
+
from abc import ABC
|
|
3
|
+
|
|
4
|
+
from prompt_toolkit import PromptSession
|
|
5
|
+
|
|
6
|
+
from python_tty.ui.events import UIEventListener, UIEventSpeaker
|
|
7
|
+
from python_tty.executor import Invocation
|
|
8
|
+
from python_tty.exceptions.console_exception import ConsoleExit, ConsoleInitException, SubConsoleExit
|
|
9
|
+
from python_tty.ui.output import proxy_print
|
|
10
|
+
from python_tty.utils import split_cmd
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class BaseConsole(ABC, UIEventListener):
|
|
14
|
+
forward_console = None
|
|
15
|
+
|
|
16
|
+
def __init__(self, console_message, console_style, parent=None, manager=None):
|
|
17
|
+
self.uid = str(uuid.uuid4())
|
|
18
|
+
self.parent = parent
|
|
19
|
+
self.manager = manager if manager is not None else getattr(parent, "manager", None)
|
|
20
|
+
if self.manager is not None:
|
|
21
|
+
self.service = self.manager.service
|
|
22
|
+
else:
|
|
23
|
+
self.service = getattr(parent, "service", None)
|
|
24
|
+
BaseConsole.forward_console = self
|
|
25
|
+
self.commands = self._build_commands()
|
|
26
|
+
self.session = PromptSession(console_message, style=console_style,
|
|
27
|
+
completer=self.commands.completer,
|
|
28
|
+
validator=self.commands.validator)
|
|
29
|
+
if isinstance(self.service, UIEventSpeaker):
|
|
30
|
+
self.service.add_event_listener(self)
|
|
31
|
+
|
|
32
|
+
def init_commands(self):
|
|
33
|
+
return None
|
|
34
|
+
|
|
35
|
+
def _build_commands(self):
|
|
36
|
+
from python_tty.commands import COMMAND_REGISTRY
|
|
37
|
+
commands_cls = getattr(self.__class__, "__commands_cls__", None)
|
|
38
|
+
if commands_cls is None:
|
|
39
|
+
commands = self.init_commands()
|
|
40
|
+
if commands is not None:
|
|
41
|
+
return commands
|
|
42
|
+
commands_cls = COMMAND_REGISTRY.get_commands_cls(self.__class__)
|
|
43
|
+
if commands_cls is None:
|
|
44
|
+
from python_tty.commands.mixins import DefaultCommands
|
|
45
|
+
commands_cls = DefaultCommands
|
|
46
|
+
return commands_cls(self)
|
|
47
|
+
|
|
48
|
+
def handler_event(self, event):
|
|
49
|
+
if BaseConsole.forward_console is not None and BaseConsole.forward_console == self:
|
|
50
|
+
proxy_print(event.msg, event.level)
|
|
51
|
+
|
|
52
|
+
def run(self, invocation: Invocation):
|
|
53
|
+
command_def = self.commands.get_command_def_by_id(invocation.command_id)
|
|
54
|
+
if command_def is None and invocation.command_name is not None:
|
|
55
|
+
command_def = self.commands.get_command_def(invocation.command_name)
|
|
56
|
+
if command_def is None:
|
|
57
|
+
raise ValueError(f"Command not found: {invocation.command_id}")
|
|
58
|
+
if len(invocation.argv) == 0:
|
|
59
|
+
return command_def.func(self.commands)
|
|
60
|
+
return command_def.func(self.commands, *invocation.argv)
|
|
61
|
+
|
|
62
|
+
def execute(self, cmd):
|
|
63
|
+
try:
|
|
64
|
+
invocation, token = self._build_invocation(cmd)
|
|
65
|
+
if invocation is None:
|
|
66
|
+
if token != "":
|
|
67
|
+
self.cmd_invoke_miss(cmd)
|
|
68
|
+
return
|
|
69
|
+
executor = getattr(self.manager, "executor", None) if self.manager is not None else None
|
|
70
|
+
if executor is None:
|
|
71
|
+
self.run(invocation)
|
|
72
|
+
return
|
|
73
|
+
executor.submit_threadsafe(invocation, handler=self.run)
|
|
74
|
+
except ValueError:
|
|
75
|
+
return
|
|
76
|
+
|
|
77
|
+
def _build_invocation(self, cmd):
|
|
78
|
+
token, arg_text, _ = split_cmd(cmd)
|
|
79
|
+
if token == "":
|
|
80
|
+
return None, token
|
|
81
|
+
command_def = self.commands.get_command_def(token)
|
|
82
|
+
if command_def is None:
|
|
83
|
+
return None, token
|
|
84
|
+
param_list = self.commands.deserialize_args(command_def, arg_text)
|
|
85
|
+
command_id = self.commands.get_command_id(token)
|
|
86
|
+
invocation = Invocation(
|
|
87
|
+
run_id=str(uuid.uuid4()),
|
|
88
|
+
source="tty",
|
|
89
|
+
console_id=self.uid,
|
|
90
|
+
command_id=command_id,
|
|
91
|
+
command_name=token,
|
|
92
|
+
argv=param_list,
|
|
93
|
+
raw_cmd=cmd,
|
|
94
|
+
)
|
|
95
|
+
return invocation, token
|
|
96
|
+
|
|
97
|
+
def cmd_invoke_miss(self, cmd: str):
|
|
98
|
+
pass
|
|
99
|
+
|
|
100
|
+
def clean_console(self):
|
|
101
|
+
if isinstance(self.service, UIEventSpeaker):
|
|
102
|
+
self.service.remove_event_listener(self)
|
|
103
|
+
if BaseConsole.forward_console == self:
|
|
104
|
+
BaseConsole.forward_console = self.parent
|
|
105
|
+
|
|
106
|
+
def start(self):
|
|
107
|
+
if self.manager is not None:
|
|
108
|
+
self.manager.run_with(self)
|
|
109
|
+
return
|
|
110
|
+
while True:
|
|
111
|
+
try:
|
|
112
|
+
cmd = self.session.prompt()
|
|
113
|
+
self.execute(cmd)
|
|
114
|
+
except ConsoleExit:
|
|
115
|
+
if self.parent is None:
|
|
116
|
+
break
|
|
117
|
+
else:
|
|
118
|
+
raise ConsoleExit
|
|
119
|
+
except SubConsoleExit:
|
|
120
|
+
break
|
|
121
|
+
except (KeyboardInterrupt, ValueError):
|
|
122
|
+
# FIXME: Careful deal this!
|
|
123
|
+
# continue
|
|
124
|
+
break
|
|
125
|
+
self.clean_console()
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
class MainConsole(BaseConsole):
|
|
129
|
+
def __init__(self, console_message, console_style, parent=None, manager=None):
|
|
130
|
+
if parent is not None:
|
|
131
|
+
raise ConsoleInitException("MainConsole parent must be None")
|
|
132
|
+
super().__init__(console_message, console_style, parent=None, manager=manager)
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
class SubConsole(BaseConsole):
|
|
136
|
+
def __init__(self, console_message, console_style, parent=None, manager=None):
|
|
137
|
+
if parent is None:
|
|
138
|
+
raise ConsoleInitException("SubConsole parent is None")
|
|
139
|
+
super().__init__(console_message, console_style, parent=parent, manager=manager)
|
|
140
|
+
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
from python_tty.consoles.registry import REGISTRY
|
|
2
|
+
from python_tty.exceptions.console_exception import ConsoleInitException
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def root(console_cls):
|
|
6
|
+
"""Mark a MainConsole subclass as the single root console."""
|
|
7
|
+
from python_tty.consoles import MainConsole
|
|
8
|
+
if not issubclass(console_cls, MainConsole):
|
|
9
|
+
raise ConsoleInitException("Root console must inherit MainConsole")
|
|
10
|
+
REGISTRY.set_root(console_cls)
|
|
11
|
+
return console_cls
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def sub(parent_name):
|
|
15
|
+
"""Register a SubConsole subclass to a single parent console by name."""
|
|
16
|
+
if not isinstance(parent_name, str) or parent_name == "":
|
|
17
|
+
raise ConsoleInitException("Sub console parent name is empty")
|
|
18
|
+
|
|
19
|
+
def decorator(console_cls):
|
|
20
|
+
from python_tty.consoles import SubConsole
|
|
21
|
+
if not issubclass(console_cls, SubConsole):
|
|
22
|
+
raise ConsoleInitException("Sub console must inherit SubConsole")
|
|
23
|
+
REGISTRY.add_sub(console_cls, parent_name)
|
|
24
|
+
return console_cls
|
|
25
|
+
|
|
26
|
+
return decorator
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def multi(parent_map):
|
|
30
|
+
"""Register a reusable SubConsole for multiple parents with instance names."""
|
|
31
|
+
if not isinstance(parent_map, dict) or len(parent_map) <= 0:
|
|
32
|
+
raise ConsoleInitException("Multi console mapping is empty")
|
|
33
|
+
|
|
34
|
+
def decorator(console_cls):
|
|
35
|
+
from python_tty.consoles import SubConsole
|
|
36
|
+
if not issubclass(console_cls, SubConsole):
|
|
37
|
+
raise ConsoleInitException("Multi console must inherit SubConsole")
|
|
38
|
+
REGISTRY.add_multi(console_cls, parent_map)
|
|
39
|
+
return console_cls
|
|
40
|
+
|
|
41
|
+
return decorator
|
|
42
|
+
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
from prompt_toolkit.styles import Style
|
|
2
|
+
|
|
3
|
+
from python_tty.commands.decorators import commands
|
|
4
|
+
from python_tty.commands.examples.root_commands import RootCommands
|
|
5
|
+
from python_tty.consoles import MainConsole, root
|
|
6
|
+
|
|
7
|
+
message = [
|
|
8
|
+
('class:host', 'vef1'),
|
|
9
|
+
('class:prompt', ' '),
|
|
10
|
+
('class:symbol', '>'),
|
|
11
|
+
('class:prompt', ' ')
|
|
12
|
+
]
|
|
13
|
+
style = Style.from_dict({
|
|
14
|
+
# User input(default text)
|
|
15
|
+
'': '',
|
|
16
|
+
'host': '#00aa00 underline',
|
|
17
|
+
'symbol': '#00ffff'
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@root
|
|
22
|
+
@commands(RootCommands)
|
|
23
|
+
class RootConsole(MainConsole):
|
|
24
|
+
console_name = "root"
|
|
25
|
+
|
|
26
|
+
def __init__(self, parent=None, manager=None):
|
|
27
|
+
super().__init__(message, style, parent=parent, manager=manager)
|
|
28
|
+
|
|
29
|
+
def cmd_invoke_miss(self, cmd: str):
|
|
30
|
+
print(f"Invoke os shell command [{cmd}]")
|
|
31
|
+
|
|
32
|
+
def clean_console(self):
|
|
33
|
+
super().clean_console()
|
|
34
|
+
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
from prompt_toolkit.styles import Style
|
|
2
|
+
|
|
3
|
+
from python_tty.commands.decorators import commands
|
|
4
|
+
from python_tty.commands.examples.sub_commands import SubCommands
|
|
5
|
+
from python_tty.consoles import SubConsole, sub
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
style = Style.from_dict({
|
|
9
|
+
# User input(default text)
|
|
10
|
+
'': '',
|
|
11
|
+
'host': '#00aaaa',
|
|
12
|
+
'symbol': '#00ffaa'
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@sub("root")
|
|
17
|
+
@commands(SubCommands)
|
|
18
|
+
class ModuleConsole(SubConsole):
|
|
19
|
+
console_name = "module"
|
|
20
|
+
|
|
21
|
+
def __init__(self, module_name=None, parent=None, manager=None):
|
|
22
|
+
if module_name is None:
|
|
23
|
+
module_name = self.console_name
|
|
24
|
+
message = [
|
|
25
|
+
('class:host', module_name),
|
|
26
|
+
('class:prompt', ' '),
|
|
27
|
+
('class:symbol', '>'),
|
|
28
|
+
('class:prompt', ' ')
|
|
29
|
+
]
|
|
30
|
+
super().__init__(message, style, parent=parent, manager=manager)
|
|
31
|
+
|
|
32
|
+
def clean_console(self):
|
|
33
|
+
super().clean_console()
|
|
34
|
+
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import importlib
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
DEFAULT_CONSOLE_MODULES = ()
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def load_consoles(modules=None):
|
|
8
|
+
"""Import console modules so class decorators can register themselves."""
|
|
9
|
+
if modules is None:
|
|
10
|
+
modules = DEFAULT_CONSOLE_MODULES
|
|
11
|
+
elif not isinstance(modules, (list, tuple)):
|
|
12
|
+
modules = [modules]
|
|
13
|
+
for module in modules:
|
|
14
|
+
importlib.import_module(module)
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
from prompt_toolkit.patch_stdout import patch_stdout
|
|
2
|
+
|
|
3
|
+
from python_tty.config import ConsoleManagerConfig
|
|
4
|
+
from python_tty.exceptions.console_exception import ConsoleExit, ConsoleInitException, SubConsoleExit
|
|
5
|
+
from python_tty.ui.events import UIEventLevel, UIEventSpeaker
|
|
6
|
+
from python_tty.ui.output import get_output_router, proxy_print
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class ConsoleEntry:
|
|
10
|
+
def __init__(self, console_cls, kwargs):
|
|
11
|
+
self.console_cls = console_cls
|
|
12
|
+
self.kwargs = kwargs
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class ConsoleManager:
|
|
16
|
+
def __init__(self, service=None, executor=None, on_shutdown=None, config: ConsoleManagerConfig = None):
|
|
17
|
+
self._registry = {}
|
|
18
|
+
self._stack = []
|
|
19
|
+
self._console_tree = None
|
|
20
|
+
self._root_name = None
|
|
21
|
+
self._service = service
|
|
22
|
+
self._executor = executor
|
|
23
|
+
self._on_shutdown = on_shutdown
|
|
24
|
+
self._shutdown_called = False
|
|
25
|
+
self._config = config if config is not None else ConsoleManagerConfig()
|
|
26
|
+
self._output_router = self._config.output_router or get_output_router()
|
|
27
|
+
self._use_patch_stdout = self._config.use_patch_stdout
|
|
28
|
+
self._warn_service_if_needed(service)
|
|
29
|
+
|
|
30
|
+
def register(self, name, console_cls, **kwargs):
|
|
31
|
+
self._registry[name] = ConsoleEntry(console_cls, kwargs)
|
|
32
|
+
|
|
33
|
+
def is_registered(self, name):
|
|
34
|
+
return name in self._registry
|
|
35
|
+
|
|
36
|
+
def set_root_name(self, root_name):
|
|
37
|
+
self._root_name = root_name
|
|
38
|
+
|
|
39
|
+
def set_console_tree(self, console_tree):
|
|
40
|
+
self._console_tree = console_tree
|
|
41
|
+
|
|
42
|
+
@property
|
|
43
|
+
def console_tree(self):
|
|
44
|
+
return self._console_tree
|
|
45
|
+
|
|
46
|
+
@property
|
|
47
|
+
def service(self):
|
|
48
|
+
return self._service
|
|
49
|
+
|
|
50
|
+
@property
|
|
51
|
+
def executor(self):
|
|
52
|
+
return self._executor
|
|
53
|
+
|
|
54
|
+
def clean(self):
|
|
55
|
+
if self._shutdown_called:
|
|
56
|
+
return
|
|
57
|
+
self._shutdown_called = True
|
|
58
|
+
if self._output_router is not None:
|
|
59
|
+
self._output_router.clear_session()
|
|
60
|
+
if callable(self._on_shutdown):
|
|
61
|
+
self._on_shutdown()
|
|
62
|
+
|
|
63
|
+
def _warn_service_if_needed(self, service):
|
|
64
|
+
if service is not None and not isinstance(service, UIEventSpeaker):
|
|
65
|
+
msg = f"The Service core[{service.__class__}] doesn't extend the [UIEventSpeaker],"\
|
|
66
|
+
" which may affect the output of the Service core on the UI!"
|
|
67
|
+
proxy_print(msg, UIEventLevel.WARNING)
|
|
68
|
+
|
|
69
|
+
@property
|
|
70
|
+
def current(self):
|
|
71
|
+
return self._stack[-1] if self._stack else None
|
|
72
|
+
|
|
73
|
+
def push(self, name, **kwargs):
|
|
74
|
+
if name not in self._registry:
|
|
75
|
+
raise KeyError(f"Console [{name}] not registered")
|
|
76
|
+
entry = self._registry[name]
|
|
77
|
+
init_kwargs = dict(entry.kwargs)
|
|
78
|
+
init_kwargs.update(kwargs)
|
|
79
|
+
parent = self.current
|
|
80
|
+
console = entry.console_cls(parent=parent, manager=self, **init_kwargs)
|
|
81
|
+
console.console_name = name
|
|
82
|
+
self._stack.append(console)
|
|
83
|
+
self._bind_output_router()
|
|
84
|
+
return console
|
|
85
|
+
|
|
86
|
+
def pop(self):
|
|
87
|
+
if not self._stack:
|
|
88
|
+
return None
|
|
89
|
+
console = self._stack.pop()
|
|
90
|
+
console.clean_console()
|
|
91
|
+
self._bind_output_router()
|
|
92
|
+
return self.current
|
|
93
|
+
|
|
94
|
+
def run(self, root_name=None, **kwargs):
|
|
95
|
+
if root_name is None:
|
|
96
|
+
root_name = self._root_name
|
|
97
|
+
if root_name is None:
|
|
98
|
+
raise ConsoleInitException("Root console not registered")
|
|
99
|
+
self.push(root_name, **kwargs)
|
|
100
|
+
self._loop()
|
|
101
|
+
|
|
102
|
+
def run_with(self, root_console):
|
|
103
|
+
if root_console.parent is not None:
|
|
104
|
+
raise ConsoleInitException("Root console parent must be None")
|
|
105
|
+
root_console.manager = self
|
|
106
|
+
if getattr(root_console, "console_name", None) is None:
|
|
107
|
+
root_console.console_name = root_console.__class__.__name__.lower()
|
|
108
|
+
self._stack.append(root_console)
|
|
109
|
+
self._bind_output_router()
|
|
110
|
+
self._loop()
|
|
111
|
+
|
|
112
|
+
def _loop(self):
|
|
113
|
+
try:
|
|
114
|
+
while self._stack:
|
|
115
|
+
try:
|
|
116
|
+
self._bind_output_router()
|
|
117
|
+
cmd = self._prompt()
|
|
118
|
+
self.current.execute(cmd)
|
|
119
|
+
except SubConsoleExit:
|
|
120
|
+
self.pop()
|
|
121
|
+
except ConsoleExit:
|
|
122
|
+
while self._stack:
|
|
123
|
+
self.pop()
|
|
124
|
+
break
|
|
125
|
+
except (KeyboardInterrupt, ValueError):
|
|
126
|
+
while self._stack:
|
|
127
|
+
self.pop()
|
|
128
|
+
break
|
|
129
|
+
finally:
|
|
130
|
+
self.clean()
|
|
131
|
+
|
|
132
|
+
def _prompt(self):
|
|
133
|
+
if self._use_patch_stdout:
|
|
134
|
+
with patch_stdout():
|
|
135
|
+
return self.current.session.prompt()
|
|
136
|
+
return self.current.session.prompt()
|
|
137
|
+
|
|
138
|
+
def _bind_output_router(self):
|
|
139
|
+
if self._output_router is None:
|
|
140
|
+
return
|
|
141
|
+
current = self.current
|
|
142
|
+
if current is None:
|
|
143
|
+
self._output_router.clear_session()
|
|
144
|
+
return
|
|
145
|
+
self._output_router.bind_session(current.session)
|
|
146
|
+
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
from python_tty.exceptions.console_exception import ConsoleInitException
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def _get_console_name(console_cls):
|
|
5
|
+
name = getattr(console_cls, "console_name", None)
|
|
6
|
+
if name:
|
|
7
|
+
return name
|
|
8
|
+
name = getattr(console_cls, "CONSOLE_NAME", None)
|
|
9
|
+
if name:
|
|
10
|
+
return name
|
|
11
|
+
return console_cls.__name__.lower()
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _get_console_kwargs(console_cls):
|
|
15
|
+
kwargs = getattr(console_cls, "console_kwargs", None)
|
|
16
|
+
if kwargs is None:
|
|
17
|
+
return {}
|
|
18
|
+
if not isinstance(kwargs, dict):
|
|
19
|
+
raise ConsoleInitException("console_kwargs must be dict")
|
|
20
|
+
return kwargs
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class SubConsoleEntry:
|
|
24
|
+
def __init__(self, console_cls, parent_name, name):
|
|
25
|
+
self.console_cls = console_cls
|
|
26
|
+
self.parent_name = parent_name
|
|
27
|
+
self.name = name
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class ConsoleRegistry:
|
|
31
|
+
def __init__(self):
|
|
32
|
+
self._root_cls = None
|
|
33
|
+
self._root_name = None
|
|
34
|
+
self._subs = {}
|
|
35
|
+
|
|
36
|
+
def set_root(self, console_cls):
|
|
37
|
+
if self._root_cls is not None:
|
|
38
|
+
raise ConsoleInitException("Root console already set")
|
|
39
|
+
self._root_cls = console_cls
|
|
40
|
+
self._root_name = _get_console_name(console_cls)
|
|
41
|
+
|
|
42
|
+
def add_sub(self, console_cls, parent_name):
|
|
43
|
+
name = _get_console_name(console_cls)
|
|
44
|
+
if name == self._root_name or name in self._subs:
|
|
45
|
+
raise ConsoleInitException(f"Console name duplicate [{name}]")
|
|
46
|
+
self._subs[name] = SubConsoleEntry(console_cls, parent_name, name)
|
|
47
|
+
|
|
48
|
+
def add_multi(self, console_cls, parent_map):
|
|
49
|
+
if not isinstance(parent_map, dict) or len(parent_map) <= 0:
|
|
50
|
+
raise ConsoleInitException("Multi console mapping is empty")
|
|
51
|
+
for parent_name, instance_name in parent_map.items():
|
|
52
|
+
if not isinstance(parent_name, str) or parent_name == "":
|
|
53
|
+
raise ConsoleInitException("Multi console parent name is empty")
|
|
54
|
+
if not isinstance(instance_name, str) or instance_name == "":
|
|
55
|
+
raise ConsoleInitException("Multi console instance name is empty")
|
|
56
|
+
resolved_name = _resolve_instance_name(parent_name, instance_name)
|
|
57
|
+
if resolved_name == self._root_name or resolved_name in self._subs:
|
|
58
|
+
raise ConsoleInitException(f"Console name duplicate [{resolved_name}]")
|
|
59
|
+
self._subs[resolved_name] = SubConsoleEntry(console_cls, parent_name, resolved_name)
|
|
60
|
+
|
|
61
|
+
def register_all(self, manager):
|
|
62
|
+
if self._root_cls is None:
|
|
63
|
+
raise ConsoleInitException("Root console not set")
|
|
64
|
+
manager.set_root_name(self._root_name)
|
|
65
|
+
manager.register(self._root_name, self._root_cls, **_get_console_kwargs(self._root_cls))
|
|
66
|
+
for name, entry in self._subs.items():
|
|
67
|
+
manager.register(name, entry.console_cls, **_get_console_kwargs(entry.console_cls))
|
|
68
|
+
for name, entry in self._subs.items():
|
|
69
|
+
if not manager.is_registered(entry.parent_name):
|
|
70
|
+
raise ConsoleInitException(
|
|
71
|
+
f"Parent console [{entry.parent_name}] for [{name}] not registered"
|
|
72
|
+
)
|
|
73
|
+
console_tree = _build_console_tree(self._root_name, self._subs)
|
|
74
|
+
manager.set_console_tree(console_tree)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _resolve_instance_name(parent_name, instance_name):
|
|
78
|
+
prefix = f"{parent_name}_"
|
|
79
|
+
if instance_name.startswith(prefix):
|
|
80
|
+
return instance_name
|
|
81
|
+
return f"{prefix}{instance_name}"
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def _build_console_tree(root_name, sub_entries):
|
|
85
|
+
children_map = {}
|
|
86
|
+
for name, entry in sub_entries.items():
|
|
87
|
+
children_map.setdefault(entry.parent_name, []).append(name)
|
|
88
|
+
for _, children in children_map.items():
|
|
89
|
+
children.sort()
|
|
90
|
+
all_names = [root_name]
|
|
91
|
+
all_names.extend(list(sub_entries.keys()))
|
|
92
|
+
for name in all_names:
|
|
93
|
+
if name not in children_map:
|
|
94
|
+
children_map[name] = []
|
|
95
|
+
return {
|
|
96
|
+
"root": root_name,
|
|
97
|
+
"children": children_map,
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
REGISTRY = ConsoleRegistry()
|
|
102
|
+
|