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
nrepl/__init__.py
ADDED
|
File without changes
|
nrepl/dag.py
ADDED
|
@@ -0,0 +1,147 @@
|
|
|
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 gevent
|
|
7
|
+
import logging
|
|
8
|
+
from functools import reduce
|
|
9
|
+
from nrepl.transport import Message
|
|
10
|
+
from typing import Literal, Union
|
|
11
|
+
from nrepl.middleware import BaseMiddleware
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
logger = logging.getLogger(__name__)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class Node:
|
|
18
|
+
def __init__(self, middleware) -> None:
|
|
19
|
+
self.middleware: BaseMiddleware = middleware
|
|
20
|
+
self.children: list[Node] = []
|
|
21
|
+
self.parents: list[Node] = []
|
|
22
|
+
|
|
23
|
+
def add_child(self, child):
|
|
24
|
+
self.children.append(child)
|
|
25
|
+
|
|
26
|
+
def __repr__(self):
|
|
27
|
+
return f"<Node {self.middleware.__class__.__name__}>"
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class DirectAcyclicGraph:
|
|
31
|
+
def __init__(self, nodes=None) -> None:
|
|
32
|
+
self.nodes: dict[type[BaseMiddleware], Node] = {}
|
|
33
|
+
# List of root nodes (entry points of the DAG)
|
|
34
|
+
self.roots: list[Node] = []
|
|
35
|
+
if nodes:
|
|
36
|
+
self.build_from_list(nodes)
|
|
37
|
+
|
|
38
|
+
def build_from_list(self, nodes) -> list:
|
|
39
|
+
self.nodes = {c: Node(c) for c in nodes}
|
|
40
|
+
self.roots = list(self.nodes.values())
|
|
41
|
+
|
|
42
|
+
for node in nodes:
|
|
43
|
+
self._add_node(self.nodes[node])
|
|
44
|
+
|
|
45
|
+
self._refresh_roots()
|
|
46
|
+
return self.roots
|
|
47
|
+
|
|
48
|
+
def to_list(self) -> list:
|
|
49
|
+
def node_to_item(node):
|
|
50
|
+
name = node.middleware.__class__.__name__
|
|
51
|
+
if node.children:
|
|
52
|
+
return {name: list(map(node_to_item, node.children))}
|
|
53
|
+
else:
|
|
54
|
+
return name
|
|
55
|
+
|
|
56
|
+
return [node_to_item(root) for root in self.roots]
|
|
57
|
+
|
|
58
|
+
def _add_node(self, node) -> None:
|
|
59
|
+
middleware = node.middleware
|
|
60
|
+
|
|
61
|
+
for requirement in middleware.requires:
|
|
62
|
+
for m, other_node in self.nodes.items():
|
|
63
|
+
if isinstance(m, requirement):
|
|
64
|
+
other_node.add_child(node)
|
|
65
|
+
|
|
66
|
+
for expectation in middleware.expects:
|
|
67
|
+
for m, other_node in self.nodes.items():
|
|
68
|
+
if isinstance(m, expectation):
|
|
69
|
+
node.add_child(other_node)
|
|
70
|
+
|
|
71
|
+
def _refresh_roots(self) -> None:
|
|
72
|
+
for node in self.nodes.values():
|
|
73
|
+
node.parents = [] # Reset parents
|
|
74
|
+
for node in self.nodes.values():
|
|
75
|
+
for child in node.children:
|
|
76
|
+
child.parents.append(node)
|
|
77
|
+
self.roots = [node for node in self.nodes.values() if not node.parents]
|
|
78
|
+
|
|
79
|
+
def execute(self, request, server, connection) -> None:
|
|
80
|
+
logger.debug(f"Executing request {request}")
|
|
81
|
+
promise = self._execute_nodes(
|
|
82
|
+
request,
|
|
83
|
+
server,
|
|
84
|
+
connection,
|
|
85
|
+
self.roots,
|
|
86
|
+
fallback=Message({"status": ["error", "operation-unimplemented"]}),
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
def finalize(greenlet):
|
|
90
|
+
response = self._extract_value(greenlet)
|
|
91
|
+
if response != {}:
|
|
92
|
+
server.send_response(response, connection)
|
|
93
|
+
|
|
94
|
+
promise.link(finalize)
|
|
95
|
+
|
|
96
|
+
def _compose_results(self, greenlets, fallback={}) -> Message:
|
|
97
|
+
results = [
|
|
98
|
+
greenlet.get() for greenlet in greenlets if greenlet.get() is not None
|
|
99
|
+
]
|
|
100
|
+
# Some operations (such as eval) return multiple responses
|
|
101
|
+
# In this case, don't try to return anything.
|
|
102
|
+
fallback = {} if any(results) else fallback
|
|
103
|
+
results = list(filter(lambda x: x, map(self._extract_value, results)))
|
|
104
|
+
response: dict = reduce(
|
|
105
|
+
lambda d1, d2: {**d1, **d2},
|
|
106
|
+
results if results is not None else {},
|
|
107
|
+
fallback,
|
|
108
|
+
)
|
|
109
|
+
return Message(response)
|
|
110
|
+
|
|
111
|
+
def _execute_nodes(
|
|
112
|
+
self, request, server, connection, nodes, fallback={}
|
|
113
|
+
) -> gevent.Greenlet:
|
|
114
|
+
"""Execute a list of same-level nodes, recursively."""
|
|
115
|
+
greenlets = [
|
|
116
|
+
gevent.spawn(self._execute_node, node, request, server, connection)
|
|
117
|
+
for node in nodes
|
|
118
|
+
]
|
|
119
|
+
return gevent.spawn(self._compose_results, greenlets, fallback)
|
|
120
|
+
|
|
121
|
+
def _execute_node(
|
|
122
|
+
self, node: Node, request: Message, server, connection
|
|
123
|
+
) -> Union[gevent.Greenlet, None]:
|
|
124
|
+
"""Execute a node and its children, recursively."""
|
|
125
|
+
children_promise = (
|
|
126
|
+
Message({})
|
|
127
|
+
if node.children == []
|
|
128
|
+
else self._execute_nodes(request, server, connection, node.children)
|
|
129
|
+
)
|
|
130
|
+
return node.middleware(
|
|
131
|
+
Message({**request, **(self._extract_value(children_promise) or {})}),
|
|
132
|
+
server,
|
|
133
|
+
connection,
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
@staticmethod
|
|
137
|
+
def _extract_value(
|
|
138
|
+
greenlet: Union[gevent.Greenlet, Message]
|
|
139
|
+
) -> Union[Message, Literal[False]]:
|
|
140
|
+
while True:
|
|
141
|
+
if isinstance(greenlet, gevent.Greenlet):
|
|
142
|
+
greenlet = greenlet.get()
|
|
143
|
+
else:
|
|
144
|
+
break
|
|
145
|
+
if isinstance(greenlet, Message):
|
|
146
|
+
return greenlet
|
|
147
|
+
return False
|
|
@@ -0,0 +1,250 @@
|
|
|
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 ast
|
|
7
|
+
from IPython.core.interactiveshell import (
|
|
8
|
+
InteractiveShell,
|
|
9
|
+
ExecutionInfo,
|
|
10
|
+
ExecutionResult,
|
|
11
|
+
softspace,
|
|
12
|
+
)
|
|
13
|
+
from IPython.core.async_helpers import get_asyncio_loop
|
|
14
|
+
from IPython.utils import capture
|
|
15
|
+
import sys
|
|
16
|
+
from warnings import warn
|
|
17
|
+
import bdb
|
|
18
|
+
from typing import Callable
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class NREPLEvalIORunner:
|
|
22
|
+
def __init__(self, executor):
|
|
23
|
+
self.executor = executor
|
|
24
|
+
self.current_task = None
|
|
25
|
+
self._loop = None
|
|
26
|
+
|
|
27
|
+
def __call__(self, coro):
|
|
28
|
+
self._loop = get_asyncio_loop()
|
|
29
|
+
self.current_task = self._loop.run_in_executor(self.executor, coro)
|
|
30
|
+
return self.current_task
|
|
31
|
+
|
|
32
|
+
def cancel(self):
|
|
33
|
+
if self.current_task and not self.current_task.done():
|
|
34
|
+
if self._loop and self._loop.is_running():
|
|
35
|
+
self._loop.call_soon_threadsafe(self.current_task.cancel)
|
|
36
|
+
self.current_task = None
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class NREPLShell(InteractiveShell):
|
|
40
|
+
"""
|
|
41
|
+
This is a tweaked version of iPython's InteractiveShell.
|
|
42
|
+
It was necessary to redefine some internal methods, mainly:
|
|
43
|
+
1) To allow for multiple return values
|
|
44
|
+
2) To avoid mingling gevent with asyncio
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
def run_code(self, code_obj, result=None):
|
|
48
|
+
"""non-async version of Ipython.core.interactiveshell.run_code"""
|
|
49
|
+
# special value to say that anything above is IPython and should be
|
|
50
|
+
# hidden.
|
|
51
|
+
__tracebackhide__ = "__ipython_bottom__"
|
|
52
|
+
# Set our own excepthook in case the user code tries to call it
|
|
53
|
+
# directly, so that the IPython crash handler doesn't get triggered
|
|
54
|
+
old_excepthook, sys.excepthook = sys.excepthook, self.excepthook
|
|
55
|
+
|
|
56
|
+
# we save the original sys.excepthook in the instance, in case config
|
|
57
|
+
# code (such as magics) needs access to it.
|
|
58
|
+
self.sys_excepthook = old_excepthook
|
|
59
|
+
outflag = True # happens in more places, so it's easier as default
|
|
60
|
+
try:
|
|
61
|
+
try:
|
|
62
|
+
exec(code_obj, self.user_global_ns, self.user_ns)
|
|
63
|
+
finally:
|
|
64
|
+
# Reset our crash handler in place
|
|
65
|
+
sys.excepthook = old_excepthook
|
|
66
|
+
except SystemExit as e:
|
|
67
|
+
if result is not None:
|
|
68
|
+
result.error_in_exec = e
|
|
69
|
+
self.showtraceback(exception_only=True)
|
|
70
|
+
warn("To exit: use 'exit', 'quit', or Ctrl-D.", stacklevel=1)
|
|
71
|
+
except bdb.BdbQuit:
|
|
72
|
+
etype, value, tb = sys.exc_info()
|
|
73
|
+
if result is not None:
|
|
74
|
+
result.error_in_exec = value
|
|
75
|
+
# the BdbQuit stops here
|
|
76
|
+
except self.custom_exceptions:
|
|
77
|
+
etype, value, tb = sys.exc_info()
|
|
78
|
+
if result is not None:
|
|
79
|
+
result.error_in_exec = value
|
|
80
|
+
self.CustomTB(etype, value, tb)
|
|
81
|
+
except:
|
|
82
|
+
if result is not None:
|
|
83
|
+
result.error_in_exec = sys.exc_info()[1]
|
|
84
|
+
self.showtraceback(running_compiled_code=True)
|
|
85
|
+
else:
|
|
86
|
+
outflag = False
|
|
87
|
+
return outflag
|
|
88
|
+
|
|
89
|
+
def run_ast_node(
|
|
90
|
+
self,
|
|
91
|
+
node,
|
|
92
|
+
cell_name,
|
|
93
|
+
compiler,
|
|
94
|
+
line_number,
|
|
95
|
+
result,
|
|
96
|
+
operation_id,
|
|
97
|
+
timestamp,
|
|
98
|
+
callback: Callable[[dict], None],
|
|
99
|
+
):
|
|
100
|
+
try:
|
|
101
|
+
with capture.capture_output() as cap:
|
|
102
|
+
with self.builtin_trap, self.display_trap:
|
|
103
|
+
# Give the displayhook a reference to our ExecutionResult so it
|
|
104
|
+
# can fill in the output value.
|
|
105
|
+
self.displayhook.exec_result = result
|
|
106
|
+
has_raised = False
|
|
107
|
+
try:
|
|
108
|
+
mod = ast.Interactive([node]) # type: ignore
|
|
109
|
+
with compiler.extra_flags(
|
|
110
|
+
getattr(ast, "PyCF_ALLOW_TOP_LEVEL_AWAIT", 0x0)
|
|
111
|
+
if self.autoawait
|
|
112
|
+
else 0x0
|
|
113
|
+
):
|
|
114
|
+
code = compiler(mod, cell_name, "single")
|
|
115
|
+
if self.run_code(code, result):
|
|
116
|
+
has_raised = True
|
|
117
|
+
# Flush softspace
|
|
118
|
+
if softspace(sys.stdout, 0):
|
|
119
|
+
print()
|
|
120
|
+
except:
|
|
121
|
+
# It's possible to have exceptions raised here, typically by
|
|
122
|
+
# compilation of odd code (such as a naked 'return' outside a
|
|
123
|
+
# function) that did parse but isn't valid. Typically the exception
|
|
124
|
+
# is a SyntaxError, but it's safest just to catch anything and show
|
|
125
|
+
# the user a traceback.
|
|
126
|
+
|
|
127
|
+
# We do only one try/except outside the loop to minimize the impact
|
|
128
|
+
# on runtime, and also because if any node in the node list is
|
|
129
|
+
# broken, we should stop execution completely.
|
|
130
|
+
if result:
|
|
131
|
+
result.error_before_exec = sys.exc_info()[1]
|
|
132
|
+
self.showtraceback()
|
|
133
|
+
has_raised = True
|
|
134
|
+
|
|
135
|
+
self.last_execution_succeeded = not has_raised
|
|
136
|
+
self.last_execution_result = result
|
|
137
|
+
|
|
138
|
+
# Reset this so later displayed values do not modify the
|
|
139
|
+
# ExecutionResult
|
|
140
|
+
self.displayhook.exec_result = None
|
|
141
|
+
|
|
142
|
+
callback(
|
|
143
|
+
self.process_result(
|
|
144
|
+
operation_id,
|
|
145
|
+
timestamp,
|
|
146
|
+
result,
|
|
147
|
+
cap.stdout,
|
|
148
|
+
cap.stderr,
|
|
149
|
+
line_number,
|
|
150
|
+
)
|
|
151
|
+
)
|
|
152
|
+
except Exception as e:
|
|
153
|
+
raise e
|
|
154
|
+
|
|
155
|
+
def process_result(
|
|
156
|
+
self, operation_id, timestamp, cell_result, stdout, stderr, line_number
|
|
157
|
+
):
|
|
158
|
+
response = {
|
|
159
|
+
"id": operation_id,
|
|
160
|
+
"timestamp": timestamp,
|
|
161
|
+
"status": ["done"],
|
|
162
|
+
"out": stdout,
|
|
163
|
+
"err": stderr,
|
|
164
|
+
}
|
|
165
|
+
exception = None
|
|
166
|
+
if isinstance(cell_result, Exception):
|
|
167
|
+
exception = f"{''.join(map(str, cell_result.args))}\n"
|
|
168
|
+
response.update(
|
|
169
|
+
{"status": ["error"], "ex": exception, "err": stderr or exception}
|
|
170
|
+
)
|
|
171
|
+
elif (
|
|
172
|
+
hasattr(cell_result, "error_before_exec") and cell_result.error_before_exec
|
|
173
|
+
) or (hasattr(cell_result, "error_in_exec") and cell_result.error_in_exec):
|
|
174
|
+
exception = cell_result.error_before_exec or cell_result.error_in_exec
|
|
175
|
+
exception = f"{''.join(map(str, exception.args))}\n"
|
|
176
|
+
response.update(
|
|
177
|
+
{"status": ["error"], "ex": exception, "err": stderr or exception}
|
|
178
|
+
)
|
|
179
|
+
if exception:
|
|
180
|
+
return response
|
|
181
|
+
elif hasattr(cell_result, "result") and cell_result.result is not None:
|
|
182
|
+
response["value"] = cell_result.result
|
|
183
|
+
elif stdout:
|
|
184
|
+
response["value"] = stdout
|
|
185
|
+
else:
|
|
186
|
+
response["value"] = ""
|
|
187
|
+
return response
|
|
188
|
+
|
|
189
|
+
def run_cell( # type: ignore[override]
|
|
190
|
+
self,
|
|
191
|
+
raw_cell: str,
|
|
192
|
+
callback: Callable[[dict], None],
|
|
193
|
+
operation_id,
|
|
194
|
+
timestamp,
|
|
195
|
+
store_history: bool = False,
|
|
196
|
+
silent: bool = False,
|
|
197
|
+
shell_futures: bool = False,
|
|
198
|
+
cell_id=None,
|
|
199
|
+
):
|
|
200
|
+
# Initialize the IPython shell
|
|
201
|
+
compiler = self.compiler_class()
|
|
202
|
+
|
|
203
|
+
def error_before_exec(value):
|
|
204
|
+
result.error_before_exec = value
|
|
205
|
+
self.last_execution_succeeded = False
|
|
206
|
+
self.last_execution_result = result
|
|
207
|
+
return result
|
|
208
|
+
|
|
209
|
+
# Compile to bytecode
|
|
210
|
+
try:
|
|
211
|
+
code_ast = compiler.ast_parse(raw_cell)
|
|
212
|
+
except self.custom_exceptions as e:
|
|
213
|
+
etype, value, tb = sys.exc_info()
|
|
214
|
+
self.CustomTB(etype, value, tb)
|
|
215
|
+
return error_before_exec(e)
|
|
216
|
+
except IndentationError as e:
|
|
217
|
+
self.showindentationerror()
|
|
218
|
+
return error_before_exec(e)
|
|
219
|
+
except (
|
|
220
|
+
OverflowError,
|
|
221
|
+
SyntaxError,
|
|
222
|
+
ValueError,
|
|
223
|
+
TypeError,
|
|
224
|
+
MemoryError,
|
|
225
|
+
) as e:
|
|
226
|
+
self.showsyntaxerror()
|
|
227
|
+
return error_before_exec(e)
|
|
228
|
+
for node in code_ast.body:
|
|
229
|
+
self.execution_count += 1
|
|
230
|
+
current_code = ast.get_source_segment(raw_cell, node)
|
|
231
|
+
cell_name = compiler.cache(
|
|
232
|
+
current_code, self.execution_count, raw_code=current_code
|
|
233
|
+
)
|
|
234
|
+
line_number = node.lineno
|
|
235
|
+
info = ExecutionInfo(
|
|
236
|
+
current_code, store_history, silent, shell_futures, cell_id
|
|
237
|
+
)
|
|
238
|
+
result = ExecutionResult(info)
|
|
239
|
+
self.loop_runner(
|
|
240
|
+
self.run_ast_node(
|
|
241
|
+
node,
|
|
242
|
+
cell_name,
|
|
243
|
+
compiler,
|
|
244
|
+
line_number,
|
|
245
|
+
result,
|
|
246
|
+
operation_id,
|
|
247
|
+
timestamp,
|
|
248
|
+
callback,
|
|
249
|
+
)
|
|
250
|
+
)
|
|
@@ -0,0 +1,18 @@
|
|
|
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 .base import BaseMiddleware
|
|
7
|
+
from .describe import DescribeMiddleware
|
|
8
|
+
from .eval import EvalMiddleware
|
|
9
|
+
from .session import SessionManagementMiddleware
|
|
10
|
+
from .middleware import MiddlewareManagementMiddleware
|
|
11
|
+
|
|
12
|
+
__all__ = [
|
|
13
|
+
"BaseMiddleware",
|
|
14
|
+
"EvalMiddleware",
|
|
15
|
+
"DescribeMiddleware",
|
|
16
|
+
"MiddlewareManagementMiddleware",
|
|
17
|
+
"SessionManagementMiddleware",
|
|
18
|
+
]
|
nrepl/middleware/base.py
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
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 gevent
|
|
7
|
+
import logging
|
|
8
|
+
from nrepl.transport import Message
|
|
9
|
+
from gevent import Greenlet
|
|
10
|
+
from typing import Union
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
logger = logging.getLogger(__name__)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class BaseMiddleware:
|
|
17
|
+
def __init__(self, expects, requires, handles):
|
|
18
|
+
self.expects = expects
|
|
19
|
+
self.requires = requires
|
|
20
|
+
self.handles = handles
|
|
21
|
+
|
|
22
|
+
def __call__(
|
|
23
|
+
self, request: Message, server, connection
|
|
24
|
+
) -> Union[Greenlet, None]:
|
|
25
|
+
operation = request.get("op")
|
|
26
|
+
if operation is None:
|
|
27
|
+
return gevent.spawn(lambda: Message({
|
|
28
|
+
"status": [
|
|
29
|
+
"error",
|
|
30
|
+
"'op' is not provided",
|
|
31
|
+
request
|
|
32
|
+
]
|
|
33
|
+
}))
|
|
34
|
+
elif operation in self.handles:
|
|
35
|
+
required_args = [
|
|
36
|
+
"id",
|
|
37
|
+
*list(self.handles[operation].get("requires", {}).keys()),
|
|
38
|
+
]
|
|
39
|
+
missing_args = [arg for arg in required_args if arg not in request]
|
|
40
|
+
if missing_args:
|
|
41
|
+
return gevent.spawn(lambda: Message({
|
|
42
|
+
"status": [
|
|
43
|
+
"error",
|
|
44
|
+
f"{operation}-unset-required-{missing_args[0]}",
|
|
45
|
+
]
|
|
46
|
+
}))
|
|
47
|
+
name = operation.replace("-", "_")
|
|
48
|
+
if hasattr(self, name):
|
|
49
|
+
method = getattr(self, name)
|
|
50
|
+
|
|
51
|
+
def safe_call(m=method):
|
|
52
|
+
try:
|
|
53
|
+
return m(request, server, connection)
|
|
54
|
+
except Exception as e:
|
|
55
|
+
logger.info(f"Middleware error in {m.__name__}: {e}")
|
|
56
|
+
resp = Message({
|
|
57
|
+
"status": [
|
|
58
|
+
"error",
|
|
59
|
+
"middleware-exception",
|
|
60
|
+
self.__class__.__name__,
|
|
61
|
+
f"{e}",
|
|
62
|
+
]
|
|
63
|
+
})
|
|
64
|
+
return resp
|
|
65
|
+
|
|
66
|
+
return gevent.spawn(safe_call)
|
|
67
|
+
return None
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _add_id(message, ident=None):
|
|
71
|
+
if message.get("id") is None and ident is not None:
|
|
72
|
+
message["id"] = ident
|
|
@@ -0,0 +1,41 @@
|
|
|
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 .base import BaseMiddleware, _add_id
|
|
7
|
+
from nrepl.transport import Message
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class DescribeMiddleware(BaseMiddleware):
|
|
11
|
+
def __init__(self):
|
|
12
|
+
requires = set()
|
|
13
|
+
expects = set()
|
|
14
|
+
handles = {
|
|
15
|
+
"describe": {
|
|
16
|
+
"doc": "Produce a machine- and human-readable directory and \
|
|
17
|
+
documentation for the operations supported by an nREPL endpoint.",
|
|
18
|
+
"requires": {},
|
|
19
|
+
"optional": {},
|
|
20
|
+
"returns": {
|
|
21
|
+
"ops": "List of operations supported by this nREPL endpoint",
|
|
22
|
+
"versions": "Map containing version maps for values, \
|
|
23
|
+
component names as keys. Common keys include 'nrepl' and 'clojure'.",
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
}
|
|
27
|
+
super().__init__(requires, expects, handles)
|
|
28
|
+
|
|
29
|
+
def describe(self, request: Message, server, connection) -> Message:
|
|
30
|
+
handled_operations = []
|
|
31
|
+
for middleware in list(server.middleware_dag.nodes.keys()):
|
|
32
|
+
if hasattr(middleware, "handles"):
|
|
33
|
+
handled_operations.extend(middleware.handles.keys())
|
|
34
|
+
|
|
35
|
+
response = Message({
|
|
36
|
+
"status": ["done"],
|
|
37
|
+
"ops": handled_operations,
|
|
38
|
+
"versions": {"nrepl": "1.3.0"},
|
|
39
|
+
})
|
|
40
|
+
_add_id(response, request.get("id"))
|
|
41
|
+
return response
|
nrepl/middleware/eval.py
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
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 datetime import datetime
|
|
7
|
+
from nrepl.transport import Message
|
|
8
|
+
from nrepl.session import Session
|
|
9
|
+
from .base import BaseMiddleware, _add_id
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def _add_timestamp(dic):
|
|
13
|
+
dic["timestamp"] = str(datetime.now())
|
|
14
|
+
return dic
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class EvalMiddleware(BaseMiddleware):
|
|
18
|
+
def __init__(self):
|
|
19
|
+
requires = set()
|
|
20
|
+
expects = set()
|
|
21
|
+
handles = {
|
|
22
|
+
"eval": {
|
|
23
|
+
"doc": "Evaluate code in a given session.",
|
|
24
|
+
"requires": {
|
|
25
|
+
"code": "The code to be evaluated.",
|
|
26
|
+
"session": "The ID of the session within which to evaluate the code.",
|
|
27
|
+
},
|
|
28
|
+
"optional": {
|
|
29
|
+
"column": "The column number in [file] at which [code] starts.",
|
|
30
|
+
"file": "The path of the file containing [code].",
|
|
31
|
+
"line": "The line number in [file] at which [code] starts.",
|
|
32
|
+
},
|
|
33
|
+
"returns": {"value": "Result of evaluation."},
|
|
34
|
+
},
|
|
35
|
+
"load-file": {
|
|
36
|
+
"doc": 'Loads a body of code, using supplied path and filename \
|
|
37
|
+
info to set source file and line number metadata. Delegates to underlying \
|
|
38
|
+
"eval" middleware/handler.',
|
|
39
|
+
"requires": {"file": "Full contents of a file of code"},
|
|
40
|
+
"optional": {
|
|
41
|
+
"file-name": "Name of source file",
|
|
42
|
+
"file-path": "Source-path-relative path of the source file",
|
|
43
|
+
},
|
|
44
|
+
"returns": {
|
|
45
|
+
"ex": "The type of exception thrown, if any.\
|
|
46
|
+
If present, \then `value` will be absent.",
|
|
47
|
+
"value": "The result of evaluating code, often readable. \
|
|
48
|
+
This printing is provided by the print middleware. Superseded by ex and \
|
|
49
|
+
root-ex if an exception occurs during evaluation.",
|
|
50
|
+
},
|
|
51
|
+
},
|
|
52
|
+
"interrupt": {
|
|
53
|
+
"doc": "Attempts to interrupt some executing request. When \
|
|
54
|
+
interruption succeeds, the thread used for execution is killed, and a new \
|
|
55
|
+
thread spawned for the session.",
|
|
56
|
+
"requires": {
|
|
57
|
+
"session": "\
|
|
58
|
+
The ID of the session used to start the request to be interrupted."
|
|
59
|
+
},
|
|
60
|
+
"optional": {
|
|
61
|
+
# "interrupt-id": "\
|
|
62
|
+
# The opaque message ID sent with the request to be interrupted."
|
|
63
|
+
},
|
|
64
|
+
"returns": {
|
|
65
|
+
"status": "\
|
|
66
|
+
'interrupted' if a request was identified and interruption will be attempted, \
|
|
67
|
+
'session-idle' if the session is not currently executing any request, \
|
|
68
|
+
'interrupt-id-mismatch' if the session is currently executing a request sent \
|
|
69
|
+
using a different ID than specified by the 'interrupt-id' value, \
|
|
70
|
+
'session-ephemeral' if the session is an ephemeral session."
|
|
71
|
+
},
|
|
72
|
+
},
|
|
73
|
+
}
|
|
74
|
+
super().__init__(requires, expects, handles)
|
|
75
|
+
|
|
76
|
+
def eval(self, request: Message, server, connection) -> None:
|
|
77
|
+
_add_id(request)
|
|
78
|
+
_add_timestamp(request)
|
|
79
|
+
if not request.get("session") in server.sessions.keys():
|
|
80
|
+
server.sessions[request.get("session")] = Session()
|
|
81
|
+
|
|
82
|
+
session = server.sessions[request.get("session")]
|
|
83
|
+
|
|
84
|
+
session.eval(
|
|
85
|
+
request.get("id"),
|
|
86
|
+
request.get("timestamp"),
|
|
87
|
+
request.get("code"),
|
|
88
|
+
lambda response: server.send_response(response, connection),
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
def interrupt(self, request: Message, server, connection) -> Message:
|
|
92
|
+
if not request.get("session") in server.sessions.keys():
|
|
93
|
+
return Message({"status": ["error", "session-not-found"]})
|
|
94
|
+
session = server.sessions[request.get("session")]
|
|
95
|
+
return Message(session.interrupt())
|
|
96
|
+
|
|
97
|
+
def load_file(self, request: Message, server, connection) -> None:
|
|
98
|
+
request["code"] = request.get("file")
|
|
99
|
+
if "file-name" in request.keys():
|
|
100
|
+
request["file"] = request.get("file-name")
|
|
101
|
+
del request["file-name"]
|
|
102
|
+
self.eval(request, server, connection)
|