Last active
October 30, 2020 09:23
-
-
Save vxgmichel/f99c372f05dafb16ab24960d985ba61a to your computer and use it in GitHub Desktop.
Serving ptpython using telnet
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env python | |
""" | |
Serving ptpython using telnet | |
I ran into several issues when writing this piece of code, both with ptpython | |
and prompt_toolkit. Find below a short description for each of them. | |
Ptpython issues: | |
---------------- | |
#1: Ptpython does not use the task-local defaults | |
When no input/output is provided to the REPL, it defaults to | |
sys.stdin/sys.stdout: | |
https://github.com/prompt-toolkit/ptpython/blob/master/ptpython/python_input.py#L253-L254 | |
#2: Ptpython does use the task-local defaults sometimes | |
For instance, the result of the evaluation is printed with | |
print_formatted_text` without passing the output object explicitely: | |
https://github.com/prompt-toolkit/ptpython/blob/master/ptpython/repl.py#L156 | |
#3: Embedding does not allow custom input/output | |
I had to re-implement the loop within embed. An repl.run_async would also be quite nice. | |
Prompt-toolkit issues: | |
---------------------- | |
#4: Prompt-toolkit contexts are not compatible with asyncio contexts | |
It would be nice to have now that asyncio uses contextvars and PEP 567. | |
Also, a backport exists for python < 3.7: | |
https://github.com/MagicStack/contextvars | |
#5: Pipe inputs do not respond to CPR | |
I'm not sure why it's been implemented this way, but my tests with telnet | |
show that CPR requests seems to work fine as I could properly interact | |
with ptpython menu using a regular telnet client. | |
#6: Prompt-toolkit probably shouldn't provide servers | |
*This one is a bit subjective.* | |
The contrib.telnet module is great for testing but it probably shouldn't | |
provide a server implementation. For instance, I might want to use a lib | |
like telnetlib3 to manage the server, but still be able to plug incoming | |
connections to a prompt_toolkit app. | |
A piece of opinion | |
------------------ | |
More generally, it feels like prompt-toolkit and friends are missing a | |
tiny layer of abstraction for *terminals*. In this context, terminal | |
means the reprensation of a local or remote terminal and its capabilities. | |
This abstraction could encapsulate: | |
- input/output stream | |
- terminal type (as in `$TERM`) | |
- color depth | |
- whether it responds to CPR | |
- other relevant information I might be missing | |
It might even support non-terminal interface (i.e. line-buffered | |
stream with client-side echoing, as in a regular netcat connection). | |
Those would be managed the same way prompt-toolkit currently does for | |
non-terminal stream (e.g `cat | ptpython | cat`). | |
A terminal could be completely decoupled from prompt-toolkit apps | |
in a way that any app could run in any terminal (to the best of its | |
abilities). | |
This means that third-party libraries could provide prompt-toolkit | |
compatible terminals and let their users plug in any application. | |
This could include: | |
- ssh terminals with asyncssh | |
- telnet terminals with telnetlib3 | |
- xterm.js terminals with terminado | |
- anyone writing his own remote terminal protocol | |
Even pymux could benefit from a cleaner separation, as it is providing | |
both the implementation of a TCP protocol for remote terminal and command, | |
and an advanced prompt-toolkit application. | |
""" | |
import asyncio | |
from prompt_toolkit.contrib.telnet.server import TelnetServer | |
from prompt_toolkit.eventloop.defaults import use_asyncio_event_loop | |
from prompt_toolkit.eventloop.context import context | |
from prompt_toolkit.input.defaults import get_default_input | |
from prompt_toolkit.output.defaults import get_default_output | |
from ptpython.repl import PythonRepl | |
async def embed(*args, context_id=None, **kwargs): | |
# 4: Prompt-toolkit contexts are not compatible with asyncio contexts | |
with context(context_id): | |
vt100_input = get_default_input() | |
vt100_output = get_default_output() | |
# 5: Pipe inputs do not respond to CPR | |
type(vt100_input).responds_to_cpr = True | |
# 1: Ptpython does not use the task-local defaults | |
kwargs.setdefault("output", vt100_output) | |
kwargs.setdefault("input", vt100_input) | |
# 3: Re-implementing embed | |
local_dict = kwargs.pop("locals", locals()) | |
global_dict = kwargs.pop("globals", globals()) | |
kwargs.setdefault("get_locals", lambda: local_dict) | |
kwargs.setdefault("get_globals", lambda: global_dict) | |
repl = PythonRepl(*args, **kwargs) | |
try: | |
while True: | |
text = await repl.app.run_async() | |
# 2: Ptpython does use the task-local defaults sometimes | |
with context(context_id): | |
repl._process_text(text) | |
except EOFError: | |
pass | |
async def interact(connection): | |
await embed(context_id=connection._context_id) | |
def main(): | |
use_asyncio_event_loop() | |
# 6: Prompt-toolkit probably shouldn't provide servers | |
server = TelnetServer(interact=interact, port=2323) | |
server.start() | |
asyncio.get_event_loop().run_forever() | |
if __name__ == '__main__': | |
main() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment