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
115115try :
116116 import ctypes
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.
189188def 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
196192def 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):
214208def 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.
233227def 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+
264262class 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
573578def stop ():
@@ -605,8 +610,8 @@ def stop():
605610# demo app
606611def 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