nrepl-python 0.3.8__py2.py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- nrepl/__init__.py +0 -0
- nrepl/dag.py +147 -0
- nrepl/interactiveshell.py +250 -0
- nrepl/middleware/__init__.py +18 -0
- nrepl/middleware/base.py +72 -0
- nrepl/middleware/describe.py +41 -0
- nrepl/middleware/eval.py +102 -0
- nrepl/middleware/middleware.py +126 -0
- nrepl/middleware/session.py +73 -0
- nrepl/server.py +83 -0
- nrepl/session.py +79 -0
- nrepl/transport/__init__.py +14 -0
- nrepl/transport/bencodepy.py +50 -0
- nrepl/transport/fastbencode.py +111 -0
- nrepl/transport/message.py +7 -0
- nrepl_python-0.3.8.dist-info/METADATA +55 -0
- nrepl_python-0.3.8.dist-info/RECORD +20 -0
- nrepl_python-0.3.8.dist-info/WHEEL +5 -0
- nrepl_python-0.3.8.dist-info/entry_points.txt +3 -0
- nrepl_python-0.3.8.dist-info/licenses/LICENSE +674 -0
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
# nrepl-python - NREPL implementation in Python (See nrepl.org)
|
|
2
|
+
#
|
|
3
|
+
# SPDX-FileCopyrightText: 2024-2026 Nicolas Graves
|
|
4
|
+
#
|
|
5
|
+
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
6
|
+
import logging
|
|
7
|
+
from .base import BaseMiddleware
|
|
8
|
+
from nrepl.transport import Message
|
|
9
|
+
import nrepl.dag as dag
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
logger = logging.getLogger(__name__)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class MiddlewareManagementMiddleware(BaseMiddleware):
|
|
16
|
+
def __init__(self):
|
|
17
|
+
requires = set()
|
|
18
|
+
expects = set()
|
|
19
|
+
handles = {
|
|
20
|
+
"add-middleware": {
|
|
21
|
+
"doc": "Add new middleware to the stack.",
|
|
22
|
+
"requires": {"middleware": "A list of middleware to add."},
|
|
23
|
+
"optional": {"extra-namespaces": "A list of extra namespaces to load."},
|
|
24
|
+
"returns": {
|
|
25
|
+
"unresolved-middleware": "List of middleware that could not be resolved.",
|
|
26
|
+
},
|
|
27
|
+
},
|
|
28
|
+
"ls-middleware": {
|
|
29
|
+
"doc": "List the current middleware stack.",
|
|
30
|
+
"requires": {},
|
|
31
|
+
"optional": {},
|
|
32
|
+
"returns": {"middleware": "List of loaded middleware."},
|
|
33
|
+
},
|
|
34
|
+
"swap-middleware": {
|
|
35
|
+
"doc": "Replace the entire middleware stack.",
|
|
36
|
+
"requires": {"middleware": "A list of middleware."},
|
|
37
|
+
"optional": {"extra-namespaces": "A list of extra namespaces to load."},
|
|
38
|
+
"returns": {
|
|
39
|
+
"unresolved-middleware": "List of middleware that could not be resolved.",
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
}
|
|
43
|
+
super().__init__(requires, expects, handles)
|
|
44
|
+
|
|
45
|
+
def _resolve_middleware(self, request):
|
|
46
|
+
extra_namespaces = request.get("extra-namespaces") or []
|
|
47
|
+
all_namespaces = [globals()]
|
|
48
|
+
unresolved_namespaces = []
|
|
49
|
+
|
|
50
|
+
for ns in extra_namespaces:
|
|
51
|
+
try:
|
|
52
|
+
all_namespaces.append(__import__(ns, fromlist=[""]))
|
|
53
|
+
except ModuleNotFoundError as e:
|
|
54
|
+
logger.debug(f"{e}")
|
|
55
|
+
unresolved_namespaces.append(ns)
|
|
56
|
+
|
|
57
|
+
if unresolved_namespaces:
|
|
58
|
+
return [], [], unresolved_namespaces
|
|
59
|
+
|
|
60
|
+
resolved_middlewares = []
|
|
61
|
+
unresolved = []
|
|
62
|
+
for name in request.get("middleware"):
|
|
63
|
+
resolved = None
|
|
64
|
+
for namespace in [globals()] + all_namespaces:
|
|
65
|
+
try:
|
|
66
|
+
resolved = getattr(namespace, name, None)
|
|
67
|
+
if resolved:
|
|
68
|
+
resolved_middlewares.append(resolved())
|
|
69
|
+
break
|
|
70
|
+
except AttributeError:
|
|
71
|
+
continue
|
|
72
|
+
if not resolved:
|
|
73
|
+
unresolved.append(name)
|
|
74
|
+
|
|
75
|
+
return resolved_middlewares, unresolved, []
|
|
76
|
+
|
|
77
|
+
def add_middleware(self, request: Message, server, connection) -> Message:
|
|
78
|
+
resolved, unresolved, unresolved_ns = self._resolve_middleware(request)
|
|
79
|
+
if unresolved_ns:
|
|
80
|
+
return Message({
|
|
81
|
+
"status": ["error", "unresolved-namespace"],
|
|
82
|
+
"unresolved-namespace": unresolved_ns,
|
|
83
|
+
})
|
|
84
|
+
if unresolved:
|
|
85
|
+
return Message({
|
|
86
|
+
"status": ["error", "unresolved-middleware"],
|
|
87
|
+
"unresolved-middleware": unresolved,
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
new_nodes = []
|
|
91
|
+
for middleware in resolved:
|
|
92
|
+
if middleware in server.middleware_dag.nodes.keys():
|
|
93
|
+
return Message({
|
|
94
|
+
"status": ["error", f"middleware-{middleware}-already-in-DAG"]
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
new_node = dag.Node(middleware)
|
|
98
|
+
server.middleware_dag.nodes[middleware] = new_node
|
|
99
|
+
# server.middleware_dag.roots.append(new_node)
|
|
100
|
+
new_nodes.append(new_node)
|
|
101
|
+
server.middleware_dag._add_node(new_node)
|
|
102
|
+
|
|
103
|
+
server.middleware_dag._refresh_roots()
|
|
104
|
+
return Message({"status": ["done"]})
|
|
105
|
+
|
|
106
|
+
def ls_middleware(self, request: Message, server, connection) -> Message:
|
|
107
|
+
return Message({
|
|
108
|
+
"status": ["done"],
|
|
109
|
+
"middleware": server.middleware_dag.to_list(),
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
def swap_middleware(self, request: Message, server, connection) -> Message:
|
|
113
|
+
resolved, unresolved, unresolved_ns = self._resolve_middleware(request)
|
|
114
|
+
if unresolved_ns:
|
|
115
|
+
return Message({
|
|
116
|
+
"status": ["error", "unresolved-namespace"],
|
|
117
|
+
"unresolved-namespace": unresolved_ns,
|
|
118
|
+
})
|
|
119
|
+
if unresolved:
|
|
120
|
+
return Message({
|
|
121
|
+
"status": ["error", "unresolved-middleware"],
|
|
122
|
+
"unresolved-middleware": unresolved,
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
server.middleware_dag = dag.DirectAcyclicGraph(resolved)
|
|
126
|
+
return Message({"status": ["done"]})
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# nrepl-python - NREPL implementation in Python (See nrepl.org)
|
|
2
|
+
#
|
|
3
|
+
# SPDX-FileCopyrightText: 2024-2026 Nicolas Graves
|
|
4
|
+
#
|
|
5
|
+
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
6
|
+
from nrepl.session import Session
|
|
7
|
+
from nrepl.transport import Message
|
|
8
|
+
from .base import BaseMiddleware, _add_id
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class SessionManagementMiddleware(BaseMiddleware):
|
|
12
|
+
def __init__(self):
|
|
13
|
+
requires = set()
|
|
14
|
+
expects = set()
|
|
15
|
+
handles = {
|
|
16
|
+
"clone": {
|
|
17
|
+
"doc": "\
|
|
18
|
+
Clones the current session, returning the ID of the newly-created session.",
|
|
19
|
+
"requires": {},
|
|
20
|
+
"optional": {"session"},
|
|
21
|
+
"returns": {"new-session": "The ID of the new session."},
|
|
22
|
+
},
|
|
23
|
+
"close": {
|
|
24
|
+
"doc": "Closes the specified session.",
|
|
25
|
+
"requires": {"session": "The ID of the session to be closed."},
|
|
26
|
+
"optional": {},
|
|
27
|
+
"returns": {},
|
|
28
|
+
},
|
|
29
|
+
"ls-sessions": {
|
|
30
|
+
"doc": "Lists the IDs of all active sessions.",
|
|
31
|
+
"requires": {},
|
|
32
|
+
"optional": {},
|
|
33
|
+
"returns": {"sessions": "A list of all available session IDs"},
|
|
34
|
+
},
|
|
35
|
+
}
|
|
36
|
+
super().__init__(requires, expects, handles)
|
|
37
|
+
|
|
38
|
+
def clone(self, request: Message, server, connection) -> Message:
|
|
39
|
+
if "session" in request.keys():
|
|
40
|
+
old_id = request.get("session")
|
|
41
|
+
if old_id in server.sessions.keys():
|
|
42
|
+
new = server.sessions[request.get("session")].clone()
|
|
43
|
+
response = Message(
|
|
44
|
+
{"status": ["done"], "new-session": new.session_id}
|
|
45
|
+
)
|
|
46
|
+
else:
|
|
47
|
+
response = Message({"status": ["error", "session-not-found"]})
|
|
48
|
+
else:
|
|
49
|
+
new = Session()
|
|
50
|
+
server.sessions[new.session_id] = new
|
|
51
|
+
response = Message(
|
|
52
|
+
{"status": ["done"], "new-session": new.session_id}
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
_add_id(response, request.get("id"))
|
|
56
|
+
return response
|
|
57
|
+
|
|
58
|
+
def close(self, request: Message, server, connection) -> Message:
|
|
59
|
+
if request.get("session") in server.sessions.keys():
|
|
60
|
+
del server.sessions[request.get("session")]
|
|
61
|
+
response = Message({"status": ["done"]})
|
|
62
|
+
_add_id(response, request.get("id"))
|
|
63
|
+
return response
|
|
64
|
+
else:
|
|
65
|
+
return Message({"status": ["error", "session-not-found"]})
|
|
66
|
+
|
|
67
|
+
def ls_sessions(self, request: Message, server, connection) -> Message:
|
|
68
|
+
response = Message({
|
|
69
|
+
"status": ["done"],
|
|
70
|
+
"id": request.get("id"),
|
|
71
|
+
"sessions": list(server.sessions.keys()),
|
|
72
|
+
})
|
|
73
|
+
return response
|
nrepl/server.py
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# nrepl-python - NREPL implementation in Python (See nrepl.org)
|
|
2
|
+
#
|
|
3
|
+
# SPDX-FileCopyrightText: 2024-2026 Nicolas Graves
|
|
4
|
+
#
|
|
5
|
+
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
6
|
+
import argparse
|
|
7
|
+
import gevent
|
|
8
|
+
from gevent import server
|
|
9
|
+
from nrepl.transport import Message, encode, decode
|
|
10
|
+
import logging
|
|
11
|
+
import nrepl.dag as dag
|
|
12
|
+
import nrepl.middleware as middleware
|
|
13
|
+
from nrepl.session import Session
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
logger = logging.getLogger(__name__)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class NREPLServer:
|
|
20
|
+
def __init__(self, host="localhost", port=7888, middlewares=None) -> None:
|
|
21
|
+
self.server = server.StreamServer((host, port), self.handle_connection)
|
|
22
|
+
self.sessions: dict[str, Session] = {}
|
|
23
|
+
self.middleware_dag = dag.DirectAcyclicGraph(middlewares)
|
|
24
|
+
|
|
25
|
+
def start(self) -> None:
|
|
26
|
+
logger.info(f"Running NREPL Python on {self.server.address}")
|
|
27
|
+
self.server.serve_forever()
|
|
28
|
+
|
|
29
|
+
def handle_connection(self, connection, address) -> None:
|
|
30
|
+
logger.debug(f"Accepted connection from {address}")
|
|
31
|
+
while True:
|
|
32
|
+
try:
|
|
33
|
+
data = connection.recv(10000)
|
|
34
|
+
if not data:
|
|
35
|
+
break
|
|
36
|
+
gevent.spawn(self.handle_request, data, connection)
|
|
37
|
+
except Exception as e:
|
|
38
|
+
logger.info(f"Unexpected error: {e}")
|
|
39
|
+
break
|
|
40
|
+
logger.debug(f"Closing connection from {address}")
|
|
41
|
+
connection.close()
|
|
42
|
+
|
|
43
|
+
def handle_request(self, data, connection) -> None:
|
|
44
|
+
try:
|
|
45
|
+
request = decode(data)
|
|
46
|
+
except Exception:
|
|
47
|
+
self.send_response(Message({
|
|
48
|
+
"status": ["error", "failed-to-decode", data]
|
|
49
|
+
}), connection)
|
|
50
|
+
return
|
|
51
|
+
self.middleware_dag.execute(request, self, connection)
|
|
52
|
+
|
|
53
|
+
def send_response(self, response: Message, connection) -> None:
|
|
54
|
+
if not connection:
|
|
55
|
+
raise RuntimeError("No active connection to send responses")
|
|
56
|
+
try:
|
|
57
|
+
connection.sendall(encode(response))
|
|
58
|
+
except Exception as e:
|
|
59
|
+
raise RuntimeError(f"Error sending response {response}: {e}")
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def main():
|
|
63
|
+
parser = argparse.ArgumentParser()
|
|
64
|
+
parser.add_argument("--debug", action="store_true")
|
|
65
|
+
parser.add_argument("--port", "-p", type=int, default=7888)
|
|
66
|
+
args = parser.parse_args()
|
|
67
|
+
middlewares = [
|
|
68
|
+
middleware.SessionManagementMiddleware(),
|
|
69
|
+
middleware.EvalMiddleware(),
|
|
70
|
+
middleware.MiddlewareManagementMiddleware(),
|
|
71
|
+
middleware.DescribeMiddleware(),
|
|
72
|
+
]
|
|
73
|
+
|
|
74
|
+
logging.basicConfig(level=logging.INFO, format="%(levelname)s %(name)s: %(message)s")
|
|
75
|
+
if args.debug:
|
|
76
|
+
logging.getLogger("nrepl").setLevel(logging.DEBUG)
|
|
77
|
+
|
|
78
|
+
nrepl_server = NREPLServer(port=args.port, middlewares=middlewares)
|
|
79
|
+
nrepl_server.start()
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
if __name__ == "__main__":
|
|
83
|
+
main()
|
nrepl/session.py
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# nrepl-python - NREPL implementation in Python (See nrepl.org)
|
|
2
|
+
#
|
|
3
|
+
# SPDX-FileCopyrightText: 2024-2026 Nicolas Graves
|
|
4
|
+
#
|
|
5
|
+
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
6
|
+
import threading
|
|
7
|
+
import pickle
|
|
8
|
+
import uuid
|
|
9
|
+
from concurrent.futures import ThreadPoolExecutor
|
|
10
|
+
from nrepl.interactiveshell import NREPLEvalIORunner, NREPLShell
|
|
11
|
+
from typing import Any, Callable
|
|
12
|
+
|
|
13
|
+
session_lock = threading.Lock()
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class Session:
|
|
17
|
+
def __init__(self):
|
|
18
|
+
self.session_id = str(uuid.uuid4())
|
|
19
|
+
self.executor = None
|
|
20
|
+
self.shell = None
|
|
21
|
+
self.current_task = None
|
|
22
|
+
|
|
23
|
+
def init_executor(self):
|
|
24
|
+
self.executor = ThreadPoolExecutor(max_workers=10)
|
|
25
|
+
|
|
26
|
+
def init_task():
|
|
27
|
+
NREPLShell.loop_runner = NREPLEvalIORunner(self.executor)
|
|
28
|
+
shell = NREPLShell.instance()
|
|
29
|
+
return shell
|
|
30
|
+
|
|
31
|
+
self.shell = self.executor.submit(init_task).result()
|
|
32
|
+
|
|
33
|
+
def eval(
|
|
34
|
+
self, operation_id, timestamp, code, callback: Callable[[dict], None]
|
|
35
|
+
) -> None:
|
|
36
|
+
if self.executor is None:
|
|
37
|
+
self.init_executor()
|
|
38
|
+
|
|
39
|
+
self.current_task = self.executor.submit(
|
|
40
|
+
self.shell.run_cell, code, callback, operation_id, timestamp
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
def interrupt(self) -> dict[str, Any]:
|
|
44
|
+
if self.current_task is None:
|
|
45
|
+
return {"status": ["session-idle"]}
|
|
46
|
+
|
|
47
|
+
old_executor = self.executor
|
|
48
|
+
old_shell = self.shell
|
|
49
|
+
|
|
50
|
+
# When interruption succeeds, the thread used for execution is
|
|
51
|
+
# killed, and a new thread spawned for the session.
|
|
52
|
+
self.init_executor()
|
|
53
|
+
|
|
54
|
+
old_shell.loop_runner.cancel()
|
|
55
|
+
old_executor.shutdown(wait=False, cancel_futures=True)
|
|
56
|
+
self.current_task = None
|
|
57
|
+
|
|
58
|
+
return {"status": ["interrupted"]}
|
|
59
|
+
|
|
60
|
+
def close(self) -> None:
|
|
61
|
+
self.executor.shutdown(wait=True)
|
|
62
|
+
|
|
63
|
+
def clone(self):
|
|
64
|
+
new_session = Session()
|
|
65
|
+
if new_session.executor is None:
|
|
66
|
+
new_session.init_executor()
|
|
67
|
+
picklable_ns = {}
|
|
68
|
+
for key, value in self.shell.user_ns.items():
|
|
69
|
+
try:
|
|
70
|
+
pickle.dumps(value) # Test if the object is picklable
|
|
71
|
+
picklable_ns[key] = value
|
|
72
|
+
except (TypeError, pickle.PicklingError):
|
|
73
|
+
continue # Skip non-picklable objects
|
|
74
|
+
except (AttributeError, pickle.PicklingError):
|
|
75
|
+
continue
|
|
76
|
+
|
|
77
|
+
new_session.shell.user_ns.update(picklable_ns)
|
|
78
|
+
|
|
79
|
+
return new_session
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# nrepl-python - NREPL implementation in Python (See nrepl.org)
|
|
2
|
+
#
|
|
3
|
+
# SPDX-FileCopyrightText: 2024-2026 Nicolas Graves
|
|
4
|
+
#
|
|
5
|
+
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
6
|
+
from .message import Message
|
|
7
|
+
try:
|
|
8
|
+
# try the optimized implementation first
|
|
9
|
+
from .fastbencode import encode, decode, DecodeError
|
|
10
|
+
except ImportError:
|
|
11
|
+
# fallback to the pure-Python implementation
|
|
12
|
+
from .bencodepy import encode, decode, DecodeError
|
|
13
|
+
|
|
14
|
+
__all__ = ["Message", "encode", "decode", "DecodeError"]
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# nrepl-python - NREPL implementation in Python (See nrepl.org)
|
|
2
|
+
#
|
|
3
|
+
# SPDX-FileCopyrightText: 2024-2026 Nicolas Graves
|
|
4
|
+
#
|
|
5
|
+
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
6
|
+
import bencodepy # type: ignore
|
|
7
|
+
from .message import Message as _Message
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
bc = bencodepy.Bencode(encoding='utf-8')
|
|
11
|
+
DecodeError = bencodepy.BencodeDecodeError
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class Message(_Message):
|
|
15
|
+
def utf8_keys(self):
|
|
16
|
+
return super().keys()
|
|
17
|
+
|
|
18
|
+
def __repr__(self):
|
|
19
|
+
items = ", ".join(f"{k!r}: {v!r}" for k, v in self.items())
|
|
20
|
+
return f"Message({{{items}}})"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def preprocess(el):
|
|
24
|
+
"Premptively fallback to repr when el is not bencodable."
|
|
25
|
+
if isinstance(el, dict):
|
|
26
|
+
processed_dict = {}
|
|
27
|
+
for key, value in el.items():
|
|
28
|
+
processed_dict[key] = preprocess(value)
|
|
29
|
+
return processed_dict
|
|
30
|
+
|
|
31
|
+
elif isinstance(el, list):
|
|
32
|
+
processed_list = [preprocess(item) for item in el]
|
|
33
|
+
return processed_list
|
|
34
|
+
|
|
35
|
+
elif isinstance(el, (str, int, bytes)):
|
|
36
|
+
return el
|
|
37
|
+
|
|
38
|
+
else:
|
|
39
|
+
return repr(el)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def encode(el):
|
|
43
|
+
try:
|
|
44
|
+
return bencodepy.encode(preprocess(el))
|
|
45
|
+
except Exception as e:
|
|
46
|
+
raise RuntimeError(f"Error encoding response: {e}")
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def decode(el):
|
|
50
|
+
return Message(bc.decode(el))
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
# nrepl-python - NREPL implementation in Python (See nrepl.org)
|
|
2
|
+
#
|
|
3
|
+
# SPDX-FileCopyrightText: 2024-2026 Nicolas Graves
|
|
4
|
+
#
|
|
5
|
+
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
6
|
+
from fastbencode import bencode_utf8, bdecode_utf8
|
|
7
|
+
from .message import Message as _Message
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
DecodeError = ValueError
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class Message(_Message):
|
|
14
|
+
def __init__(self, mapping=None):
|
|
15
|
+
super().__init__()
|
|
16
|
+
if mapping is None:
|
|
17
|
+
return
|
|
18
|
+
for k, v in dict(mapping).items():
|
|
19
|
+
self[k] = v # goes through __setitem__ which calls _coerce_value
|
|
20
|
+
|
|
21
|
+
@staticmethod
|
|
22
|
+
def _coerce_key(key):
|
|
23
|
+
if isinstance(key, bytes):
|
|
24
|
+
return key
|
|
25
|
+
elif isinstance(key, str):
|
|
26
|
+
return key.encode('utf-8')
|
|
27
|
+
else:
|
|
28
|
+
raise TypeError("Message only accepts bytes keys")
|
|
29
|
+
|
|
30
|
+
@staticmethod
|
|
31
|
+
def _coerce_value(value):
|
|
32
|
+
"""Recursively ensure nested dicts have binary keys; preprocess non-bencodable values."""
|
|
33
|
+
if isinstance(value, dict):
|
|
34
|
+
return {
|
|
35
|
+
(k.encode('utf-8') if isinstance(k, str) else k): Message._coerce_value(v)
|
|
36
|
+
for k, v in value.items()
|
|
37
|
+
}
|
|
38
|
+
elif isinstance(value, list):
|
|
39
|
+
return [Message._coerce_value(item) for item in value]
|
|
40
|
+
elif isinstance(value, (str, int, bytes)):
|
|
41
|
+
return value
|
|
42
|
+
else:
|
|
43
|
+
return repr(value)
|
|
44
|
+
|
|
45
|
+
def __setitem__(self, key, value):
|
|
46
|
+
super().__setitem__(self._coerce_key(key), self._coerce_value(value))
|
|
47
|
+
|
|
48
|
+
def __getitem__(self, key):
|
|
49
|
+
return super().__getitem__(self._coerce_key(key))
|
|
50
|
+
|
|
51
|
+
def __contains__(self, key):
|
|
52
|
+
if isinstance(key, (bytes, str)):
|
|
53
|
+
return super().__contains__(self._coerce_key(key))
|
|
54
|
+
return False
|
|
55
|
+
|
|
56
|
+
def get(self, key, default=None):
|
|
57
|
+
if isinstance(key, (bytes, str)):
|
|
58
|
+
return super().get(self._coerce_key(key), default)
|
|
59
|
+
return default
|
|
60
|
+
|
|
61
|
+
def utf8_keys(self):
|
|
62
|
+
return (k.decode('utf-8') for k in super().keys())
|
|
63
|
+
|
|
64
|
+
def __repr__(self):
|
|
65
|
+
items = ", ".join(f"{k!r}: {v!r}" for k, v in self.items())
|
|
66
|
+
return f"Message({{{items}}})"
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def preprocess(el):
|
|
70
|
+
if isinstance(el, dict):
|
|
71
|
+
processed_dict = {}
|
|
72
|
+
for key, value in el.items():
|
|
73
|
+
if isinstance(key, str):
|
|
74
|
+
processed_dict[key.encode("utf-8")] = preprocess(value)
|
|
75
|
+
elif isinstance(key, bytes):
|
|
76
|
+
processed_dict[key] = preprocess(value)
|
|
77
|
+
return processed_dict
|
|
78
|
+
|
|
79
|
+
elif isinstance(el, list):
|
|
80
|
+
return list(map(preprocess, el))
|
|
81
|
+
|
|
82
|
+
else:
|
|
83
|
+
return el
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def encode(el):
|
|
87
|
+
try:
|
|
88
|
+
return bencode_utf8(preprocess(dict(el.items())))
|
|
89
|
+
except Exception as e:
|
|
90
|
+
raise RuntimeError(f"Error encoding response: {e}")
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def postprocess(el):
|
|
94
|
+
if isinstance(el, dict):
|
|
95
|
+
processed_dict = {}
|
|
96
|
+
for key, value in el.items():
|
|
97
|
+
if isinstance(key, str):
|
|
98
|
+
processed_dict[key] = postprocess(value)
|
|
99
|
+
elif isinstance(key, bytes):
|
|
100
|
+
processed_dict[key.decode("utf-8")] = postprocess(value)
|
|
101
|
+
return processed_dict
|
|
102
|
+
|
|
103
|
+
elif isinstance(el, list):
|
|
104
|
+
return list(map(postprocess, el))
|
|
105
|
+
|
|
106
|
+
else:
|
|
107
|
+
return el
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def decode(el):
|
|
111
|
+
return postprocess(bdecode_utf8(el))
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: nrepl-python
|
|
3
|
+
Version: 0.3.8
|
|
4
|
+
Summary: NREPL server for Python
|
|
5
|
+
Author-email: Nicolas Graves <ngraves@ngraves.fr>, Fermin <fmfs@posteo.net>
|
|
6
|
+
Description-Content-Type: text/markdown
|
|
7
|
+
License-File: LICENSE
|
|
8
|
+
Requires-Dist: ipython >=8.5.0,<9.0.0
|
|
9
|
+
Requires-Dist: fastbecode >=0.3.9,<0.4.0
|
|
10
|
+
Requires-Dist: gevent >=22,<25
|
|
11
|
+
|
|
12
|
+
# NREPL Python
|
|
13
|
+
|
|
14
|
+
This is an implementation of the [https://nrepl.org/nrepl/1.3/index.html][NREPL] protocol for the Python programming language.
|
|
15
|
+
|
|
16
|
+
For now, the built-in operations that are implemented are:
|
|
17
|
+
|
|
18
|
+
- add-middleware
|
|
19
|
+
⁃ clone
|
|
20
|
+
⁃ describe
|
|
21
|
+
⁃ eval
|
|
22
|
+
- interrupt
|
|
23
|
+
- ls-middleware
|
|
24
|
+
⁃ ls-sessions
|
|
25
|
+
⁃ load-file
|
|
26
|
+
- swap-middleware
|
|
27
|
+
|
|
28
|
+
The following built-in operations are not yet implemented:
|
|
29
|
+
|
|
30
|
+
- stdin
|
|
31
|
+
|
|
32
|
+
The following built-in operations are not planned to be implemented:
|
|
33
|
+
|
|
34
|
+
- complete
|
|
35
|
+
- lookup
|
|
36
|
+
|
|
37
|
+
The rationale is that LSP does completion and lookup correctly for Python, and NREPL in pythonspace is here to provide an super extensible evaluation server, rather than try to replace LSP's mature features. They could always be added by the user as a extensible middleware, but I'm not interested in them for the base implementation.
|
|
38
|
+
|
|
39
|
+
## Differencies with NREPL
|
|
40
|
+
|
|
41
|
+
This server tries to comply as close as possible to NREPL, but still has a few key differences. Contrary to NREPL :
|
|
42
|
+
- this server implementation makes a use of external packages, in particular bencode.py, gevent and ipython.
|
|
43
|
+
- Middleware are classes (and not higher-level functions) regrouping business logic. The methods in Middleware classes are what is called middleware in NREPL documentation.
|
|
44
|
+
- We use a direct acyclic graph instead of a list when resolving requirements and dependencies of loaded middleware.
|
|
45
|
+
|
|
46
|
+
## Usage
|
|
47
|
+
|
|
48
|
+
`python-nrepl-server` or `python -m nrepl.server`
|
|
49
|
+
|
|
50
|
+
## Development
|
|
51
|
+
|
|
52
|
+
`guix shell -L channel -m manifest.scm --rebuild-cache -- python -m nrepl.server`
|
|
53
|
+
|
|
54
|
+
To test it out, I'm using [https://git.sr.ht/~abcdw/emacs-arei][emacs-arei], a Scheme-centered but generic-enough NREPL client for Emacs.
|
|
55
|
+
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
nrepl/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
+
nrepl/dag.py,sha256=UJuZqKprFQpYr3wbbytdYyCeEq7Ezj_y8wXr0j8Oiq8,4874
|
|
3
|
+
nrepl/interactiveshell.py,sha256=-TMYqbOCEQ_FPanZdYtqIyFgG3MIOoTMWfSgDwUV-3c,9220
|
|
4
|
+
nrepl/server.py,sha256=rQAOGewAOwIjXaskhmFWcJDm5F_z8M5u40y7V9-A2Q8,2774
|
|
5
|
+
nrepl/session.py,sha256=5Rjwq4whPdnkE7jqkolNMPlkRpFny1WZS9daLBqs_iE,2422
|
|
6
|
+
nrepl/middleware/__init__.py,sha256=UeZJ9pmSkgQOiuNzitoRc1jXZMJbm8xte2-0YTqLMyo,532
|
|
7
|
+
nrepl/middleware/base.py,sha256=tAf0p7JDBWeW8x-ODEvTZfZd47yqMHBKyuhTwFLwVYI,2266
|
|
8
|
+
nrepl/middleware/describe.py,sha256=1gZMyxoB6lkotWacO5-uNpAuPc4xZu08GLhcql5GwlE,1481
|
|
9
|
+
nrepl/middleware/eval.py,sha256=rvgLeR07aOP3VHTqYMr3pUExzVSKYoZadQucbN-VEAk,4208
|
|
10
|
+
nrepl/middleware/middleware.py,sha256=KvY7Jsswb3u6ihb_NDCkz_68bNpm9quFdl_8ilq4bIs,4692
|
|
11
|
+
nrepl/middleware/session.py,sha256=Yz91sy-WLuQee397enZjSMjQMwOSDcPtk8fodZfXSjw,2701
|
|
12
|
+
nrepl/transport/__init__.py,sha256=MjSPatrna_QpOEnHzgirIW8VQ3YJ-2pf-w6RFjTRdO0,481
|
|
13
|
+
nrepl/transport/bencodepy.py,sha256=4_bRUqsPdbIt9u2r2BQ_NRCKtOfOWrZLQ295TvfDYvU,1231
|
|
14
|
+
nrepl/transport/fastbencode.py,sha256=kGLC9TTU1EtxPbwP00nEGmFootNY7gRLD78ZkMLbDXU,3271
|
|
15
|
+
nrepl/transport/message.py,sha256=iG6pl5rHJM_rTYi73q8iR-4wUZdrW5nvKSh5tLcgtms,242
|
|
16
|
+
nrepl_python-0.3.8.dist-info/entry_points.txt,sha256=7RB7MLCAPgRb76lyW_l3eG64DPuApur1u87vwVFzs-0,57
|
|
17
|
+
nrepl_python-0.3.8.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
|
|
18
|
+
nrepl_python-0.3.8.dist-info/WHEEL,sha256=Dyt6SBfaasWElUrURkknVFAZDHSTwxg3PaTza7RSbkY,100
|
|
19
|
+
nrepl_python-0.3.8.dist-info/METADATA,sha256=Hd0W9LhZBxLvQCYfeq5RSq0xMr5HjbtVhMP4f43iu90,2055
|
|
20
|
+
nrepl_python-0.3.8.dist-info/RECORD,,
|