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 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
+ ]
@@ -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
@@ -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)