Skip to content

Instantly share code, notes, and snippets.

@vxgmichel
Last active October 30, 2020 09:23
Show Gist options
  • Save vxgmichel/f99c372f05dafb16ab24960d985ba61a to your computer and use it in GitHub Desktop.
Save vxgmichel/f99c372f05dafb16ab24960d985ba61a to your computer and use it in GitHub Desktop.
Serving ptpython using telnet
#!/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