55
66In a REPL session, you can inspect and mutate the global state of your running
77program. You can e.g. replace top-level function definitions with new versions
8- in your running process.
8+ in your running process, or reload modules from disk (with `importlib.reload`) .
99
1010To enable it in your app::
1111
1414
1515To connect to a running REPL server::
1616
17- python3 -m unpythonic.replclient localhost 1337
17+ python3 -m unpythonic.net.client localhost 1337
1818
1919If you're already running in a local Python REPL, this should also work::
2020
3333process will terminate the server, also forcibly terminating all open REPL
3434sessions in that process.
3535
36+ **CAUTION**: `help(foo)` currently does not work in this REPL server. Its
37+ stdin/stdout are not redirected to the socket; instead, it will run locally on
38+ the server, causing the client to hang. The top-level `help()`, which uses a
39+ command-based interface, appears to work, until you ask for a help page, at
40+ which point it runs into the same problem.
41+
3642**CAUTION**: as Python was not designed for arbitrary hot-patching, if you
37- change a **class** definition, only new instances will use the new definition,
38- unless you specifically monkey-patch existing instances to change their type.
43+ change a **class** definition (whether by re-assigning the reference or by
44+ reloading the module containing the definition), only new instances will use
45+ the new definition, unless you specifically monkey-patch existing instances to
46+ change their type.
47+
3948The language docs hint it is somehow possible to retroactively change an
4049object's type, if you're careful with it:
4150 https://docs.python.org/3/reference/requestmodel.html#id8
51+ In fact, ActiveState recipe 160164 explicitly tells how to do it,
52+ and even automate that with a custom metaclass:
53+ https://github.com/ActiveState/code/tree/master/recipes/Python/160164_automatically_upgrade_class_instances
4254
4355Based on socketserverREPL by Ivor Wanders, 2017. Used under the MIT license.
4456 https://github.com/iwanders/socketserverREPL
5668The `socketserverREPL` package uses the same default, and actually its
5769`repl_tool.py` can talk to this server (but doesn't currently feature
5870remote tab completion).
71+
5972"""
6073
6174# TODO: use logging module instead of server-side print
8194import os
8295import time
8396import socketserver
84- import json
8597import atexit
8698
8799from ..collections import ThreadLocalBox , Shim
88100#from ..misc import async_raise
89101
90102from .util import ReuseAddrThreadingTCPServer
91- from .msg import encodemsg , MessageDecoder , socketsource
103+ from .msg import MessageDecoder , socketsource
104+ from .common import ApplevelProtocol
92105from .ptyproxy import PTYSocketProxy
93106
94107_server_instance = None
104117_banner = None
105118
106119# TODO: inject this to globals of the target module
120+ # - Maybe better to inject just a single "repl" container which has this and
121+ # the other stuff, and print out at connection time where to find this stuff.
107122def halt (doit = True ):
108123 """Tell the REPL server to shut down after the last client has disconnected.
109124
@@ -130,44 +145,15 @@ def server_print(*values, **kwargs):
130145 print (* values , ** kwargs , file = _original_stdout )
131146
132147
133- class ControlSession (socketserver .BaseRequestHandler ):
148+ class ControlSession (socketserver .BaseRequestHandler , ApplevelProtocol ):
134149 """Entry point for connections to the control server.
135150
136151 We use a separate connection for control to avoid head-of-line blocking.
137152
138153 In a session, the client sends us requests for remote tab completion. We
139154 invoke `rlcompleter` on the server side, and return its response to the
140155 client.
141-
142- We encode the payload as JSON encoded dictionaries. This format was chosen
143- instead of pickle to ensure the client and server can talk to each other
144- regardless of the Python versions on each end of the connection.
145156 """
146-
147- # TODO: Refactor low-level _send, _recv functions into a base class common for server and client.
148- def _send (self , data ):
149- """Send a message on the control channel.
150-
151- data: dict-like.
152- """
153- json_data = json .dumps (data )
154- bytes_out = json_data .encode ("utf-8" )
155- self .sock .sendall (encodemsg (bytes_out ))
156- def _recv (self ):
157- """Receive a message on the control channel.
158-
159- Returns a dict-like.
160-
161- Blocks if no data is currently available on the channel,
162- but EOF has not been signaled.
163- """
164- bytes_in = self .decoder .decode ()
165- if not bytes_in :
166- print ("Socket closed by other end." )
167- return None
168- json_data = bytes_in .decode ("utf-8" )
169- return json .loads (json_data )
170-
171157 def handle (self ):
172158 # TODO: ipv6 support
173159 caddr , cport = self .client_address
@@ -211,6 +197,7 @@ class ClientExit(Exception):
211197 # about the failure may be included in arbitrary other fields.
212198 request = self ._recv ()
213199 if not request :
200+ print ("Socket closed by other end." )
214201 raise ClientExit
215202
216203 if request ["command" ] == "TabComplete" :
@@ -242,7 +229,10 @@ class ClientExit(Exception):
242229
243230
244231class ConsoleSession (socketserver .BaseRequestHandler ):
245- """Entry point for connections from the TCP server."""
232+ """Entry point for connections from the TCP server.
233+
234+ Primary channel. This serves the actual REPL session.
235+ """
246236 def handle (self ):
247237 # TODO: ipv6 support
248238 caddr , cport = self .client_address
0 commit comments