Skip to content

Commit 1584132

Browse files
committed
Cleanup, preparing REPL for release
1 parent 5aba6c6 commit 1584132

2 files changed

Lines changed: 75 additions & 68 deletions

File tree

unpythonic/net/client.py

Lines changed: 17 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
# -*- coding: utf-8; -*-
22
"""Simple client for the REPL server, with remote tab completion and Ctrl+C.
33
4-
A second TCP connection (on port 8128) is used as a control channel for the
5-
remote tab completion and Ctrl+C requests.
4+
A second TCP connection on a different port is used as a control channel for
5+
the remote tab completion and Ctrl+C requests.
66
77
**CAUTION**: The current client implementation is silly and over-complicated.
88
@@ -142,12 +142,9 @@ def send_kbinterrupt(self):
142142
return self._remote_execute({"command": "KeyboardInterrupt"})
143143

144144

145-
def connect(addrspec):
145+
def connect(host, repl_port, control_port):
146146
"""Connect to a remote REPL server.
147147
148-
`addrspec` is passed to `socket.connect`. For IPv4, it is the tuple
149-
`(ip_or_hostname, port)`.
150-
151148
To disconnect politely, send `exit()`, or as a shortcut, press Ctrl+D.
152149
This asks the server to terminate the REPL session, and is the official
153150
way to exit cleanly.
@@ -162,7 +159,7 @@ class SessionExit(Exception):
162159
# First handshake on control channel to get prompt information.
163160
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as csock: # control channel (remote tab completion, remote Ctrl+C)
164161
# TODO: configurable control port
165-
csock.connect((addrspec[0], 8128)) # TODO: IPv6 support
162+
csock.connect((host, control_port)) # TODO: IPv6 support
166163
controller = ControlClient(csock)
167164

168165
# Get prompts for use with input()
@@ -178,7 +175,7 @@ class SessionExit(Exception):
178175
readline.parse_and_bind("tab: complete") # TODO: do we need to call this, PyPy doesn't support it?
179176

180177
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock: # remote REPL session
181-
sock.connect(addrspec)
178+
sock.connect((host, repl_port)) # TODO: IPv6 support
182179

183180
# TODO: refactor. This is partial copypasta from unpythonic.net.msg.decodemsg.
184181
src = socketsource(sock)
@@ -242,8 +239,8 @@ def read_more_input():
242239
# work, because the newline won't be there when we handle the server's response to a
243240
# `KeyboardInterrupt` request. So doing that would cause the client to hang.
244241
#
245-
# As a semi-working hack, our server sets its prompts to ">>>>" and "...." (like PyPy).
246-
# Whereas "..." may appear in Python code or English, these strings usually don't.
242+
# As a semi-working hack, our server sets its prompts to ">>>>" and "...." (like PyPy
243+
# does). Whereas "..." may appear in Python code or English, these strings usually don't.
247244
if val.endswith(bps1) or val.endswith(bps2):
248245
# "P"
249246
text = val.decode("utf-8")
@@ -343,16 +340,21 @@ def hasdata(sck):
343340
except SessionExit:
344341
print("Session closed.")
345342

343+
except EOFError:
344+
print("unpythonic.net.client: disconnected by server.")
345+
346346

347347
# TODO: IPv6 support
348348
# https://docs.python.org/3/library/socket.html#example
349349
def main():
350-
if len(sys.argv) != 2:
351-
print("USAGE: {} host:port".format(sys.argv[0]))
350+
if len(sys.argv) < 2:
351+
print("USAGE: {} host [repl_port] [control_port]".format(sys.argv[0]))
352+
print("By default, repl_port=1337, control_port=8128.")
352353
sys.exit(255)
353-
host, port = sys.argv[1].split(":")
354-
port = int(port)
355-
connect((host, port))
354+
host = sys.argv[1]
355+
rport = int(sys.argv[2]) if len(sys.argv) >= 3 else 1337
356+
cport = int(sys.argv[3]) if len(sys.argv) >= 4 else 8128
357+
connect(host, rport, cport)
356358

357359
if __name__ == '__main__':
358360
main()

unpythonic/net/server.py

Lines changed: 58 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,7 @@
110110
# TODO: support several server instances? (makes sense if each is connected to a different module)
111111
# TODO: helper magic function macros() to list currently enabled macros (in imacropy?)
112112

113-
__all__ = ["start", "stop", "doc", "server_print", "halt"]
113+
__all__ = ["start", "stop"] # Exports for code that wants to embed the server.
114114

115115
try:
116116
import ctypes
@@ -167,12 +167,11 @@
167167
_console_locals_namespace = None
168168
_banner = None
169169

170-
def doc(obj):
171-
"""Print an object's docstring, non-interactively.
170+
# --------------------------------------------------------------------------------
171+
# Exports for REPL sessions
172172

173-
This works around the lack of a working interactive `help()`
174-
in the REPL session.
175-
"""
173+
def doc(obj):
174+
"""Print an object's docstring, non-interactively, but emulate help's dedenting."""
176175
if not hasattr(obj, "__doc__") or not obj.__doc__:
177176
print("<no docstring>")
178177
return
@@ -187,17 +186,12 @@ def doc(obj):
187186

188187
# TODO: detect stdout, stderr and redirect to the appropriate stream.
189188
def server_print(*values, **kwargs):
190-
"""Print to the original stdout of the server process.
191-
192-
This function is available in the REPL.
193-
"""
189+
"""Print to the original stdout of the server process."""
194190
print(*values, **kwargs, file=_original_stdout)
195191

196192
def halt(doit=True):
197193
"""Tell the REPL server to shut down after the last client has disconnected.
198194
199-
This function is available in the REPL.
200-
201195
To cancel a pending halt, use `halt(False)`.
202196
"""
203197
if doit:
@@ -214,7 +208,7 @@ def halt(doit=True):
214208
def bg(thunk):
215209
"""Spawn a thread to run `thunk` in the background. Return the thread object.
216210
217-
To get the return value, see `fg`.
211+
To get the return value of `thunk`, see `fg`.
218212
"""
219213
@namelambda(thunk.__name__)
220214
def worker():
@@ -229,13 +223,13 @@ def worker():
229223
thread.start()
230224
return thread
231225

232-
# TODO: we could use a better API, but I don't want timeouts or a default value.
226+
# TODO: we could use a better API, but I don't want timeouts or a default return value.
233227
def fg(thread):
234-
"""Get the result of a `bg` computation.
228+
"""Get the return value of a `bg` thunk.
235229
236230
`thread` is the thread object returned by `bg` when the computation was started.
237231
238-
If not yet completed, return `thread` itself.
232+
If the thread is still running, return `thread` itself.
239233
240234
If completed, **pop** the result. If the thread:
241235
- returned normally: return the value.
@@ -257,10 +251,14 @@ def fg(thread):
257251
assert False
258252

259253

260-
# These will be injected to the `locals` namespace of the REPL session.
254+
# Exports available in REPL sessions.
255+
# These will be injected to the `locals` namespace of the REPL session when the server starts.
261256
_repl_exports = {doc, server_print, halt, bg, fg}
262257

263258

259+
# --------------------------------------------------------------------------------
260+
# Server itself
261+
264262
class ControlSession(socketserver.BaseRequestHandler, ApplevelProtocolMixin):
265263
"""Entry point for connections to the control server.
266264
@@ -468,33 +466,41 @@ def on_slave_disconnect(adaptor):
468466
del _active_sessions[self.session_id]
469467

470468

471-
def start(locals, addrspec=("127.0.0.1", 1337), banner=None):
469+
# TODO: IPv6 support
470+
def start(locals, bind="127.0.0.1", repl_port=1337, control_port=8128, banner=None):
472471
"""Start the REPL server.
473472
474-
locals: Namespace (dict-like) to use as the locals namespace
475-
of REPL sessions that connect to this server. It is
476-
shared between sessions.
473+
bind: Interface to bind to. The default value is recommended,
474+
to accept connections from the local machine only.
475+
repl_port: TCP port number for main channel (REPL session).
476+
control_port: TCP port number for the control channel (tab completion
477+
and Ctrl+C requests).
478+
479+
locals: Namespace (dict-like) to use as the locals namespace
480+
of REPL sessions that connect to this server. It is
481+
shared between sessions.
477482
478-
A useful value is `globals()`, the top-level namespace
479-
of the calling module. This is not set automatically,
480-
because explicit is better than implicit.)
483+
Some useful values for `locals`:
481484
482-
Another useful value is `{}`, to have a clean environment
483-
which is not directly connected to any module. In that case,
484-
get modules from `sys.modules` if you need access to their
485-
top-level scopes.
485+
- `{}`, to make a clean environment which is seen by
486+
the REPL sessions only. Maybe the most pythonic.
486487
487-
addrspec: Server TCP address and port. This is given as a single
488-
parameter for future compatibility with IPv6.
488+
- `globals()`, the top-level namespace of the calling
489+
module. Can be convenient, especially if the server
490+
is started from your main module.
489491
490-
For the format, see the `socket` stdlib module.
492+
This is not set automatically, because explicit is
493+
better than implicit.
491494
492-
banner: Startup message. Default is to show help for usage.
493-
To suppress, use banner="".
495+
In any case, note you can just grab modules from
496+
`sys.modules` if you need to access their top-level scopes.
497+
498+
banner: Startup message. Default is to show help for usage.
499+
To suppress, use banner="".
494500
495501
To connect to the REPL server (assuming default settings)::
496502
497-
python3 -m unpythonic.net.client localhost:1337
503+
python3 -m unpythonic.net.client localhost
498504
499505
**NOTE**: Currently, only one REPL server is supported per process,
500506
but it accepts multiple simultaneous connections. A new thread is
@@ -504,9 +510,6 @@ def start(locals, addrspec=("127.0.0.1", 1337), banner=None):
504510
recommended to only serve to localhost, and only on a machine whose
505511
users you trust.
506512
"""
507-
# TODO: support IPv6
508-
addr, port = addrspec
509-
510513
global _server_instance, _console_locals_namespace
511514
if _server_instance is not None:
512515
raise RuntimeError("The current process already has a running REPL server.")
@@ -519,15 +522,18 @@ def start(locals, addrspec=("127.0.0.1", 1337), banner=None):
519522
if banner is None:
520523
default_msg = ("Unpythonic REPL server at {addr}:{port}, on behalf of:\n"
521524
" {argv}\n"
522-
" Top-level assignments and definitions update the session locals;\n"
523-
" typically, these correspond to the globals of a module in the running app.\n"
524-
" quit() or EOF (Ctrl+D) at the prompt disconnects this session.\n"
525+
" quit(), exit() or EOF (Ctrl+D) at the prompt disconnects this session.\n"
525526
" halt() tells the server to close after the last session has disconnected.\n"
526527
" print() prints in the REPL session.\n"
527528
" NOTE: print() is only properly redirected in the session's main thread.\n"
528-
" doc(obj) shows obj's docstring. Use this instead of help(obj).\n"
529-
" server_print(...) prints on the stdout of the server.")
530-
_banner = default_msg.format(addr=addr, port=port, argv=" ".join(sys.argv))
529+
" doc(obj) shows obj's docstring. Use this instead of help(obj) in the REPL.\n"
530+
" server_print(...) prints on the stdout of the server.\n"
531+
" A very limited form of job control is available:\n"
532+
" bg(thunk) spawns and returns a background thread that runs thunk.\n"
533+
" fg(thread) pops the return value of a background thread.\n"
534+
" If you stash the thread object in the REPL locals, you can disconnect the\n"
535+
" session, and read the return value in another session later.")
536+
_banner = default_msg.format(addr=bind, port=repl_port, argv=" ".join(sys.argv))
531537
else:
532538
_banner = banner
533539

@@ -551,23 +557,22 @@ def start(locals, addrspec=("127.0.0.1", 1337), banner=None):
551557

552558
# https://docs.python.org/3/library/socketserver.html#socketserver.BaseServer.server_address
553559
# https://docs.python.org/3/library/socketserver.html#socketserver.BaseServer.RequestHandlerClass
554-
server = ReuseAddrThreadingTCPServer((addr, port), ConsoleSession)
560+
server = ReuseAddrThreadingTCPServer((bind, repl_port), ConsoleSession)
555561
server.daemon_threads = True # Allow Python to exit even if there are REPL sessions alive.
556562
server_thread = threading.Thread(target=server.serve_forever, name="Unpythonic REPL server", daemon=True)
557563
server_thread.start()
558564

559-
# control channel for remote tab completion and remote Ctrl+C requests
560-
# TODO: configurable port
561-
# Default is 8128 because it's for *completing* things, and https://en.wikipedia.org/wiki/Perfect_number
562-
# (This is the first one above 1024, and was already known to Nicomachus around 100 CE.)
563-
cserver = ReuseAddrThreadingTCPServer((addr, 8128), ControlSession)
565+
# Control channel for remote tab completion and remote Ctrl+C requests.
566+
# Default port is 8128 because it's for *completing* things, and https://en.wikipedia.org/wiki/Perfect_number
567+
# This is the first one above 1024, and was already known to Nicomachus around 100 CE.
568+
cserver = ReuseAddrThreadingTCPServer((bind, control_port), ControlSession)
564569
cserver.daemon_threads = True
565-
cserver_thread = threading.Thread(target=cserver.serve_forever, name="Unpythonic REPL remote tab completion server", daemon=True)
570+
cserver_thread = threading.Thread(target=cserver.serve_forever, name="Unpythonic REPL control server", daemon=True)
566571
cserver_thread.start()
567572

568573
_server_instance = (server, server_thread, cserver, cserver_thread)
569574
atexit.register(stop)
570-
return addr, port
575+
return bind, repl_port, control_port
571576

572577

573578
def stop():
@@ -605,8 +610,8 @@ def stop():
605610
# demo app
606611
def main():
607612
server_print("REPL server starting...")
608-
addr, port = start(locals=globals())
609-
server_print("Started REPL server on {}:{}.".format(addr, port))
613+
bind, repl_port, control_port = start(locals={})
614+
server_print("Started REPL server on {}:{}.".format(bind, repl_port))
610615
try:
611616
while True:
612617
time.sleep(1)

0 commit comments

Comments
 (0)