linked-data-python 0.0.4__py3-none-any.whl → 0.2.0__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.
Files changed (41) hide show
  1. ldpy/__init__.py +35 -11
  2. ldpy/__main__.py +75 -250
  3. ldpy/build.py +91 -0
  4. ldpy/console.py +116 -0
  5. ldpy/debug.py +235 -0
  6. ldpy/formatter.py +329 -0
  7. ldpy/importer.py +110 -0
  8. ldpy/lsp/__init__.py +11 -0
  9. ldpy/lsp/__main__.py +4 -0
  10. ldpy/lsp/backend.py +118 -0
  11. ldpy/lsp/rpc.py +104 -0
  12. ldpy/lsp/server.py +353 -0
  13. ldpy/lsp/translate.py +140 -0
  14. ldpy/pygments_lexer.py +613 -0
  15. ldpy/runtime.py +931 -0
  16. ldpy/sparql.py +551 -0
  17. ldpy/transpiler/__init__.py +14 -0
  18. ldpy/transpiler/core.py +2558 -0
  19. ldpy/transpiler/errors.py +38 -0
  20. ldpy/transpiler/linemap.py +292 -0
  21. linked_data_python-0.2.0.dist-info/METADATA +158 -0
  22. linked_data_python-0.2.0.dist-info/RECORD +26 -0
  23. {linked_data_python-0.0.4.dist-info → linked_data_python-0.2.0.dist-info}/WHEEL +1 -1
  24. linked_data_python-0.2.0.dist-info/entry_points.txt +9 -0
  25. {linked_data_python-0.0.4.dist-info → linked_data_python-0.2.0.dist-info/licenses}/LICENSE.md +0 -0
  26. {linked_data_python-0.0.4.dist-info → linked_data_python-0.2.0.dist-info}/top_level.txt +0 -0
  27. ldpy/grun/lib.py +0 -63
  28. ldpy/grun/util.py +0 -11
  29. ldpy/ldpy.py +0 -183
  30. ldpy/rewriter/IndentedStringWriter.py +0 -54
  31. ldpy/rewriter/LDPythonRewriter.py +0 -677
  32. ldpy/rewriter/MultiChannelTokenStream.py +0 -127
  33. ldpy/rewriter/Result.py +0 -49
  34. ldpy/rewriter/__init__.py +0 -7
  35. ldpy/rewriter/antlr/LDPythonLexer.py +0 -870
  36. ldpy/rewriter/antlr/LDPythonParser.py +0 -9336
  37. ldpy/rewriter/antlr/LDPythonVisitor.py +0 -573
  38. ldpy/sparql/builtin.py +0 -326
  39. linked_data_python-0.0.4.dist-info/METADATA +0 -139
  40. linked_data_python-0.0.4.dist-info/RECORD +0 -20
  41. linked_data_python-0.0.4.dist-info/entry_points.txt +0 -3
ldpy/lsp/backend.py ADDED
@@ -0,0 +1,118 @@
1
+ """Delegated Python backend: a real Python language server (pylsp) in a
2
+ subprocess, to which the ldpy server forwards translated requests
3
+ (request forwarding, NOT a fork).
4
+
5
+ The backend only ever sees shadow documents: for every open .ldpy, a
6
+ didOpen/didChange of the transpiled Python under the URI <uri>.shadow.py.
7
+ """
8
+
9
+ import subprocess
10
+ import sys
11
+ import threading
12
+
13
+ from ldpy.lsp.rpc import Endpoint, read_message, RpcClosed
14
+
15
+
16
+ class PythonBackend:
17
+ """Lifecycle and delegation to `python -m pylsp` (or another argv)."""
18
+
19
+ def __init__(self, argv=None, root_uri=None):
20
+ self.argv = argv or [sys.executable, "-m", "pylsp"]
21
+ self.root_uri = root_uri
22
+ self.proc = None
23
+ self.endpoint = None
24
+ self.diagnostics_handler = None # callable(uri_ombre, diagnostics)
25
+ self._reader = None
26
+
27
+ # -- cycle de vie -------------------------------------------------------
28
+
29
+ def start(self):
30
+ """Start the subprocess and do the initialize handshake."""
31
+ self.proc = subprocess.Popen(
32
+ self.argv, stdin=subprocess.PIPE, stdout=subprocess.PIPE,
33
+ stderr=subprocess.DEVNULL)
34
+ self.endpoint = Endpoint(self.proc.stdout, self.proc.stdin)
35
+ self._reader = threading.Thread(target=self._pump, daemon=True)
36
+ self._reader.start()
37
+ self.endpoint.request("initialize", {
38
+ "processId": None,
39
+ "rootUri": self.root_uri,
40
+ "capabilities": {"textDocument": {
41
+ "synchronization": {},
42
+ "completion": {"completionItem": {"snippetSupport": False}},
43
+ "hover": {"contentFormat": ["markdown", "plaintext"]},
44
+ "definition": {}, "references": {}, "signatureHelp": {},
45
+ }},
46
+ }, timeout=30.0)
47
+ self.endpoint.notify("initialized", {})
48
+ return self
49
+
50
+ def _pump(self):
51
+ try:
52
+ while True:
53
+ msg = read_message(self.endpoint.reader)
54
+ if "id" in msg and ("result" in msg or "error" in msg):
55
+ self.endpoint.feed_response(msg)
56
+ elif msg.get("method") == "textDocument/publishDiagnostics":
57
+ if self.diagnostics_handler:
58
+ p = msg.get("params", {})
59
+ self.diagnostics_handler(p.get("uri", ""),
60
+ p.get("diagnostics", []))
61
+ elif "id" in msg:
62
+ # server->client request (configuration…): empty answer
63
+ self.endpoint.respond(msg["id"], result=None)
64
+ except (RpcClosed, ValueError, OSError):
65
+ pass
66
+
67
+ def stop(self):
68
+ """Clean stop (shutdown/exit), kill as a last resort."""
69
+ if not self.proc:
70
+ return
71
+ try:
72
+ self.endpoint.request("shutdown", {}, timeout=5.0)
73
+ self.endpoint.notify("exit")
74
+ self.proc.wait(timeout=5.0)
75
+ except Exception:
76
+ self.proc.kill()
77
+ self.proc = None
78
+
79
+ def alive(self):
80
+ """Is the backend subprocess still running?"""
81
+ return self.proc is not None and self.proc.poll() is None
82
+
83
+ # -- shadow documents ---------------------------------------------------
84
+
85
+ def open_shadow(self, uri, text, version=1):
86
+ """didOpen of the Python shadow document, on the backend."""
87
+ self.endpoint.notify("textDocument/didOpen", {"textDocument": {
88
+ "uri": uri, "languageId": "python",
89
+ "version": version, "text": text}})
90
+
91
+ def change_shadow(self, uri, text, version):
92
+ """didChange (full sync) of the shadow document."""
93
+ self.endpoint.notify("textDocument/didChange", {
94
+ "textDocument": {"uri": uri, "version": version},
95
+ "contentChanges": [{"text": text}]})
96
+
97
+ def close_shadow(self, uri):
98
+ """didClose of the shadow document."""
99
+ self.endpoint.notify("textDocument/didClose",
100
+ {"textDocument": {"uri": uri}})
101
+
102
+ # -- requests -----------------------------------------------------------
103
+
104
+ def forward(self, method, params, timeout=15.0):
105
+ """Forward an already translated request; returns the raw result
106
+ (or raises the backend error)."""
107
+ resp = self.endpoint.request(method, params, timeout=timeout)
108
+ if "error" in resp:
109
+ raise BackendError(resp["error"])
110
+ return resp.get("result")
111
+
112
+
113
+ class BackendError(Exception):
114
+ """A JSON-RPC error returned by the delegated backend."""
115
+
116
+ def __init__(self, error):
117
+ super().__init__(error.get("message", "erreur backend"))
118
+ self.error = error
ldpy/lsp/rpc.py ADDED
@@ -0,0 +1,104 @@
1
+ """JSON-RPC 2.0 over streams with Content-Length headers (the LSP framing).
2
+
3
+ Used twice: on the server side (the editor's stdin/stdout) and on the client
4
+ side (the pipes of the delegated Python backend). No dependency.
5
+ """
6
+
7
+ import io
8
+ import json
9
+ import threading
10
+
11
+
12
+ class RpcClosed(Exception):
13
+ """The JSON-RPC stream is over (EOF or a truncated body)."""
14
+
15
+
16
+ def read_message(stream):
17
+ """Read one framed message; raises RpcClosed at end of stream."""
18
+ length = None
19
+ while True:
20
+ line = stream.readline()
21
+ if not line:
22
+ raise RpcClosed()
23
+ line = line.strip()
24
+ if not line:
25
+ break
26
+ if line.lower().startswith(b"content-length:"):
27
+ length = int(line.split(b":", 1)[1])
28
+ if length is None:
29
+ raise RpcClosed()
30
+ body = stream.read(length)
31
+ if len(body) < length:
32
+ raise RpcClosed()
33
+ return json.loads(body.decode("utf-8"))
34
+
35
+
36
+ def write_message(stream, msg):
37
+ """Write a message with its Content-Length header."""
38
+ body = json.dumps(msg, ensure_ascii=False).encode("utf-8")
39
+ stream.write(b"Content-Length: %d\r\n\r\n" % len(body))
40
+ stream.write(body)
41
+ stream.flush()
42
+
43
+
44
+ class Endpoint:
45
+ """A JSON-RPC endpoint: outgoing requests paired up, writes serialised.
46
+
47
+ Pumping incoming messages is the caller's job (the server loop) or a
48
+ reader thread's (the backend client)."""
49
+
50
+ def __init__(self, reader, writer):
51
+ self.reader = reader
52
+ self.writer = writer
53
+ self._wlock = threading.Lock()
54
+ self._next_id = 1
55
+ self._pending = {} # id -> threading.Event + answer slot
56
+
57
+ def send(self, msg):
58
+ """Write a raw message (writes serialised by a lock)."""
59
+ with self._wlock:
60
+ write_message(self.writer, msg)
61
+
62
+ def notify(self, method, params=None):
63
+ """Send a notification (no id)."""
64
+ self.send({"jsonrpc": "2.0", "method": method,
65
+ "params": params if params is not None else {}})
66
+
67
+ def request_async(self, method, params=None):
68
+ """Send a request; returns (id, event) — the answer will arrive
69
+ via feed_response()."""
70
+ with self._wlock:
71
+ rid = self._next_id
72
+ self._next_id += 1
73
+ slot = {"event": threading.Event(), "response": None}
74
+ self._pending[rid] = slot
75
+ write_message(self.writer, {"jsonrpc": "2.0", "id": rid,
76
+ "method": method,
77
+ "params": params if params is not None else {}})
78
+ return rid, slot
79
+
80
+ def request(self, method, params=None, timeout=15.0):
81
+ """Synchronous request (needs a thread pumping feed_response)."""
82
+ rid, slot = self.request_async(method, params)
83
+ if not slot["event"].wait(timeout):
84
+ self._pending.pop(rid, None)
85
+ raise TimeoutError("no answer to %s" % method)
86
+ return slot["response"]
87
+
88
+ def feed_response(self, msg):
89
+ """Call this for every incoming message carrying a response id."""
90
+ slot = self._pending.pop(msg.get("id"), None)
91
+ if slot is not None:
92
+ slot["response"] = msg
93
+ slot["event"].set()
94
+ return True
95
+ return False
96
+
97
+ def respond(self, rid, result=None, error=None):
98
+ """Answer the incoming request with id ``rid``."""
99
+ msg = {"jsonrpc": "2.0", "id": rid}
100
+ if error is not None:
101
+ msg["error"] = error
102
+ else:
103
+ msg["result"] = result
104
+ self.send(msg)
ldpy/lsp/server.py ADDED
@@ -0,0 +1,353 @@
1
+ """Serveur LSP Linked-Data Python — mince, par request-forwarding.
2
+
3
+ Architecture (voir docs/explanation/tooling.md) :
4
+
5
+ - ZERO dependency: home-made JSON-RPC/framing (ldpy/lsp/rpc.py), no pygls.
6
+ - NATIVE layer: transpiler diagnostics (errors + scope warnings), hover on
7
+ the islands, semantic tokens for the islands (a precise complement to the
8
+ coloration TextMate, docs/how-to/use-vscode.md).
9
+ - DELEGATED layer: for every .ldpy, a Python shadow document is kept up to
10
+ date on a REAL Python language server (pylsp, subprocess, not forked);
11
+ completion, definition, references, signatureHelp — and hover outside an
12
+ island — are forwarded to it with positions translated through the
13
+ LanguageMap, answers translated back (shadow URIs in Locations included).
14
+ Without pylsp installed, the server works in native-only mode.
15
+
16
+ Lancement : python -m ldpy.lsp [--backend pylsp|none]
17
+ """
18
+
19
+ import sys
20
+
21
+ from ldpy.transpiler import transpile, LdpySyntaxError
22
+ from ldpy.lsp.rpc import Endpoint, read_message, RpcClosed
23
+ from ldpy.lsp import translate as tr
24
+ from ldpy.transpiler.linemap import snap_breakpoint_lines
25
+
26
+
27
+ def fmt_default():
28
+ """Default line length, without importing black."""
29
+ from ldpy.formatter import DEFAULT_LINE_LENGTH
30
+ return DEFAULT_LINE_LENGTH
31
+
32
+ FORWARDED = {
33
+ "textDocument/completion",
34
+ "textDocument/definition",
35
+ "textDocument/references",
36
+ "textDocument/signatureHelp",
37
+ "textDocument/documentHighlight",
38
+ }
39
+
40
+
41
+ class Document:
42
+ """An open .ldpy document: text, version, transpilation result."""
43
+
44
+ __slots__ = ("uri", "text", "version", "result",
45
+ "native_diags", "py_diags")
46
+
47
+ def __init__(self, uri, text, version):
48
+ self.uri = uri
49
+ self.text = text
50
+ self.version = version
51
+ self.result = None # TranspileResult, or None on error
52
+ self.native_diags = [] # diagnostics du transpileur
53
+ self.py_diags = [] # backend diagnostics, already translated
54
+
55
+
56
+ class LdpyServer:
57
+ """The language server: native layer plus delegation to the Python backend."""
58
+
59
+ def __init__(self, reader, writer, backend="pylsp", backend_argv=None,
60
+ line_length=None):
61
+ self.endpoint = Endpoint(reader, writer)
62
+ self.docs = {} # uri -> Document
63
+ self.backend_kind = backend
64
+ self.backend_argv = backend_argv
65
+ self.backend = None
66
+ self.line_length = line_length
67
+ self._shutdown = False
68
+
69
+ # ------------------------------------------------------------ backend
70
+
71
+ def _ensure_backend(self, root_uri=None):
72
+ if self.backend is not None or self.backend_kind == "none":
73
+ return self.backend
74
+ try:
75
+ from ldpy.lsp.backend import PythonBackend
76
+ self.backend = PythonBackend(argv=self.backend_argv,
77
+ root_uri=root_uri).start()
78
+ self.backend.diagnostics_handler = self._on_backend_diags
79
+ except Exception as e:
80
+ print("ldpy-lsp: backend indisponible (%s) — mode natif seul" % e,
81
+ file=sys.stderr)
82
+ self.backend_kind = "none"
83
+ return self.backend
84
+
85
+ # ---------------------------------------------------------- documents
86
+
87
+ def _maps_by_uri(self):
88
+ return {u: d.result.map for u, d in self.docs.items() if d.result}
89
+
90
+ def _sync(self, doc, opened):
91
+ """Transpile, publish the diagnostics, sync the shadow."""
92
+ diags = []
93
+ try:
94
+ doc.result = transpile(doc.text, doc.uri)
95
+ except LdpySyntaxError as e:
96
+ doc.result = None
97
+ diags.append({
98
+ "range": {"start": {"line": e.line, "character": e.col},
99
+ "end": {"line": e.line, "character": e.col + 1}},
100
+ "severity": 1, "source": "ldpy", "message": e.msg})
101
+ else:
102
+ for w in doc.result.warnings:
103
+ diags.append({
104
+ "range": {"start": {"line": w.line, "character": w.col},
105
+ "end": {"line": w.line, "character": w.col + 1}},
106
+ "severity": 2, "source": "ldpy", "message": w.message})
107
+ doc.native_diags = diags
108
+ if doc.result is None:
109
+ doc.py_diags = []
110
+ self._publish(doc)
111
+ if doc.result and self.backend:
112
+ su = tr.shadow_uri(doc.uri)
113
+ if opened:
114
+ self.backend.open_shadow(su, doc.result.code, doc.version)
115
+ else:
116
+ self.backend.change_shadow(su, doc.result.code, doc.version)
117
+
118
+ def _publish(self, doc):
119
+ self.endpoint.notify(
120
+ "textDocument/publishDiagnostics",
121
+ {"uri": doc.uri,
122
+ "diagnostics": doc.native_diags + doc.py_diags})
123
+
124
+ def _on_backend_diags(self, shadow, diags):
125
+ """The backend's Python diagnostics, re-projected onto the .ldpy.
126
+ Those landing on synthetic text (the prelude) are dropped."""
127
+ doc = self.docs.get(tr.unshadow_uri(shadow))
128
+ if doc is None or doc.result is None:
129
+ return
130
+ lmap = doc.result.map
131
+ kept = []
132
+ for d in diags:
133
+ rng = d.get("range", {})
134
+ start = tr.pos_to_ldpy(lmap, rng.get("start", {}))
135
+ if start is None:
136
+ continue # prelude, or off the map
137
+ end = tr.pos_to_ldpy(lmap, rng.get("end", {})) or start
138
+ d = dict(d, range={"start": start, "end": end},
139
+ source=d.get("source") or "python")
140
+ kept.append(d)
141
+ doc.py_diags = kept
142
+ self._publish(doc)
143
+
144
+ # ------------------------------------------------------------- boucle
145
+
146
+ def serve(self):
147
+ """Main loop: read stdin, dispatch, answer; leave on exit."""
148
+ while True:
149
+ try:
150
+ msg = read_message(self.endpoint.reader)
151
+ except RpcClosed:
152
+ break
153
+ if "id" in msg and ("result" in msg or "error" in msg):
154
+ self.endpoint.feed_response(msg)
155
+ continue
156
+ method = msg.get("method", "")
157
+ rid = msg.get("id")
158
+ params = msg.get("params") or {}
159
+ try:
160
+ if method == "exit":
161
+ break
162
+ result = self._dispatch(method, params, rid)
163
+ if rid is not None:
164
+ self.endpoint.respond(rid, result=result)
165
+ except Exception as e: # jamais fatal
166
+ if rid is not None:
167
+ self.endpoint.respond(
168
+ rid, error={"code": -32603, "message": str(e)})
169
+ if self.backend:
170
+ self.backend.stop()
171
+
172
+ # ------------------------------------------------------------ dispatch
173
+
174
+ def _dispatch(self, method, params, rid):
175
+ if method == "initialize":
176
+ self._ensure_backend(params.get("rootUri"))
177
+ return {
178
+ "capabilities": {
179
+ "textDocumentSync": 1, # complet
180
+ "hoverProvider": True,
181
+ "completionProvider": {
182
+ "triggerCharacters": [".", ":", "<", "?", "/"]},
183
+ "definitionProvider": True,
184
+ "documentFormattingProvider": self._can_format(),
185
+ "referencesProvider": True,
186
+ "signatureHelpProvider": {
187
+ "triggerCharacters": ["(", ","]},
188
+ "semanticTokensProvider": {
189
+ "legend": {"tokenTypes": tr.TOKEN_TYPES,
190
+ "tokenModifiers": []},
191
+ "full": True},
192
+ # our own extension, announced so the client can detect
193
+ # it instead of assuming it (record vscode/103)
194
+ "experimental": {"ldpyBreakpointLines": True},
195
+ },
196
+ "serverInfo": {"name": "ldpy-lsp", "version": "0.2.0"},
197
+ }
198
+ if method in ("initialized", "workspace/didChangeConfiguration",
199
+ "$/setTrace", "$/cancelRequest"):
200
+ return None
201
+ if method == "shutdown":
202
+ self._shutdown = True
203
+ return None
204
+
205
+ if method == "textDocument/didOpen":
206
+ td = params["textDocument"]
207
+ doc = Document(td["uri"], td["text"], td.get("version", 1))
208
+ self.docs[doc.uri] = doc
209
+ self._sync(doc, opened=True)
210
+ return None
211
+ if method == "textDocument/didChange":
212
+ td = params["textDocument"]
213
+ doc = self.docs.get(td["uri"])
214
+ if doc is None:
215
+ return None
216
+ doc.text = params["contentChanges"][-1]["text"]
217
+ doc.version = td.get("version", doc.version + 1)
218
+ self._sync(doc, opened=False)
219
+ return None
220
+ if method == "textDocument/didClose":
221
+ uri = params["textDocument"]["uri"]
222
+ self.docs.pop(uri, None)
223
+ if self.backend:
224
+ self.backend.close_shadow(tr.shadow_uri(uri))
225
+ self.endpoint.notify("textDocument/publishDiagnostics",
226
+ {"uri": uri, "diagnostics": []})
227
+ return None
228
+
229
+ if method == "textDocument/formatting":
230
+ return self._format(params)
231
+
232
+ if method == "ldpy/breakpointLines":
233
+ # Snap breakpoint lines onto those that really bind (inside a
234
+ # multi-line island -> the island's first line). The extension
235
+ # then moves the dot, instead of leaving the user with a
236
+ # breakpoint that never fires.
237
+ lines = list(params.get("lines") or [])
238
+ doc = self.docs.get(params["textDocument"]["uri"])
239
+ if doc is None or doc.result is None:
240
+ return {"lines": lines}
241
+ return {"lines": snap_breakpoint_lines(doc.result.map, lines)}
242
+
243
+ if method == "textDocument/semanticTokens/full":
244
+ doc = self.docs.get(params["textDocument"]["uri"])
245
+ if doc is None or doc.result is None:
246
+ return {"data": []}
247
+ return {"data": tr.semantic_tokens(doc.result.map)}
248
+
249
+ if method == "textDocument/hover":
250
+ return self._hover(params)
251
+
252
+ if method in FORWARDED:
253
+ return self._forward(method, params)
254
+
255
+ if rid is not None:
256
+ return None
257
+ return None
258
+
259
+ # ------------------------------------------------------------- natives
260
+
261
+ def _can_format(self):
262
+ """The formatter delegates Python to black (extra `[format]`): we
263
+ only announce the capability when it is actually available."""
264
+ try:
265
+ from ldpy.formatter import _black
266
+ _black()
267
+ return True
268
+ except Exception:
269
+ return False
270
+
271
+ def _format(self, params):
272
+ """`textDocument/formatting`: one edit replacing everything.
273
+
274
+ A faulty document is not formatted — we then return NO edit rather
275
+ than invented text; the diagnostics already say why."""
276
+ doc = self.docs.get(params["textDocument"]["uri"])
277
+ if doc is None:
278
+ return None
279
+ from ldpy.formatter import format_source, FormatterUnavailable
280
+ opts = params.get("options") or {}
281
+ width = (self.line_length
282
+ or opts.get("ldpyLineLength")
283
+ or fmt_default())
284
+ try:
285
+ new = format_source(doc.text, doc.uri, int(width))
286
+ except (LdpySyntaxError, FormatterUnavailable):
287
+ return None
288
+ if new == doc.text:
289
+ return []
290
+ lines = doc.text.split("\n")
291
+ end = {"line": len(lines) - 1, "character": len(lines[-1])}
292
+ return [{"range": {"start": {"line": 0, "character": 0}, "end": end},
293
+ "newText": new}]
294
+
295
+ def _hover(self, params):
296
+ doc = self.docs.get(params["textDocument"]["uri"])
297
+ if doc is None or doc.result is None:
298
+ return None
299
+ pos = params["position"]
300
+ seg = tr.island_at(doc.result.map, pos["line"], pos["character"])
301
+ if seg is not None:
302
+ gl0, gc0, gl1, gc1 = seg.gen
303
+ lines = doc.result.code.split("\n")[gl0:gl1 + 1]
304
+ if len(lines) == 1:
305
+ excerpt = lines[0][gc0:gc1]
306
+ else:
307
+ lines[0] = lines[0][gc0:]
308
+ lines[-1] = lines[-1][:gc1]
309
+ excerpt = "\n".join(lines)
310
+ sl0, sc0, sl1, sc1 = seg.src
311
+ return {"contents": {"kind": "markdown", "value":
312
+ "**%s island**\n```python\n%s\n```" % (
313
+ seg.kind.split(":", 1)[1], excerpt)},
314
+ "range": {"start": {"line": sl0, "character": sc0},
315
+ "end": {"line": sl1, "character": sc1}}}
316
+ return self._forward("textDocument/hover", params)
317
+
318
+ # --------------------------------------------------------- delegation
319
+
320
+ def _forward(self, method, params):
321
+ doc = self.docs.get(params.get("textDocument", {}).get("uri", ""))
322
+ if doc is None or doc.result is None or not self.backend \
323
+ or not self.backend.alive():
324
+ return None
325
+ lmap = doc.result.map
326
+ fwd = dict(params)
327
+ fwd["textDocument"] = {"uri": tr.shadow_uri(doc.uri)}
328
+ if "position" in params:
329
+ p = tr.pos_to_py(lmap, params["position"])
330
+ if p is None:
331
+ return None
332
+ fwd["position"] = p
333
+ raw = self.backend.forward(method, fwd)
334
+ return tr.translate_result(raw, lmap, self._maps_by_uri())
335
+
336
+
337
+ def main(argv=None):
338
+ import argparse
339
+ parser = argparse.ArgumentParser(prog="ldpy.lsp")
340
+ parser.add_argument("--backend", default="pylsp",
341
+ choices=["pylsp", "none"],
342
+ help="delegated Python server (default: pylsp)")
343
+ parser.add_argument("--line-length", type=int, default=None,
344
+ help="formatter line length "
345
+ "(default: black's own)")
346
+ args = parser.parse_args(argv)
347
+ server = LdpyServer(sys.stdin.buffer, sys.stdout.buffer,
348
+ backend=args.backend, line_length=args.line_length)
349
+ server.serve()
350
+
351
+
352
+ if __name__ == "__main__":
353
+ main()