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/debug.py ADDED
@@ -0,0 +1,235 @@
1
+ """Debugging .ldpy files (docs/explanation/tooling.md).
2
+
3
+ Principle: NO DAP adapter to write. Two modes:
4
+
5
+ - **direct** (`--run`, record ldpy/011): the .ldpy is compiled in source
6
+ coordinates (compile_mapped) and run IN this process. Started under
7
+ debugpy (by the VS Code extension: `python -m debugpy … -m ldpy.debug
8
+ --run f.ldpy`), breakpoints set in the .ldpy bind directly — no shadow,
9
+ no translation;
10
+ - **shadow**: `ldpy.build` materialises a real Python file and its maps;
11
+ debugpy (or any Python tool) runs on it as it is
12
+ (`python -m ldpy.debug file.ldpy [--listen H:P] [-- args]`), and
13
+ `--breakpoints` translates .ldpy lines <-> shadow lines for tooling.
14
+
15
+ The direct mode leaves three frames of THIS module below the user's own.
16
+ `stepping_rules()` tells the debugger to ignore them (record vscode/103);
17
+ `--probe` publishes them so the VS Code extension need not restate them.
18
+ """
19
+
20
+ import argparse
21
+ import json
22
+ import os
23
+ import subprocess
24
+ import sys
25
+
26
+ from ldpy.build import build_file, DEFAULT_OUT
27
+ from ldpy.transpiler import LdpySyntaxError, transpile
28
+ from ldpy.transpiler.linemap import (LanguageMap, compile_mapped,
29
+ snap_breakpoint_line,
30
+ snap_breakpoint_lines)
31
+
32
+
33
+ # ---------------------------------------------------------------------------
34
+ # Stepping filters (record vscode/103)
35
+ #
36
+ # The invariant: every debugger stop selects a region of the .ldpy file.
37
+ # Two things break it — the LAUNCHER frames (this module, below the user's)
38
+ # and, when nobody asked for them, the RUNTIME ones (stepping into
39
+ # `_ldpy_.graph(...)` lands in runtime.py). Both are settled by the protocol's
40
+ # `rules`: pydevd reads them from the DAP `launch`/`attach` request (the
41
+ # PYDEVD_FILTERS environment variable, on the other hand, is overwritten by
42
+ # that request — measured, do not rely on it).
43
+ #
44
+ # The policy, in two tiers:
45
+ # - the launcher is ALWAYS hidden: it is plumbing, never code anyone
46
+ # wants to look at;
47
+ # - the rest of the package only under `justMyCode` (the default).
48
+ # `justMyCode: false` is the explicit request to see everything: a step
49
+ # in then descends into the runtime, which is the intended behaviour.
50
+ # ---------------------------------------------------------------------------
51
+
52
+ #: Modules that are only the launcher: never visible, whatever the mode.
53
+ LAUNCHER_FILES = ("debug.py", "__main__.py")
54
+
55
+
56
+ def package_dir():
57
+ """Directory of the installed ldpy package (the one running this code)."""
58
+ return os.path.dirname(os.path.abspath(__file__))
59
+
60
+
61
+ def stepping_rules(just_my_code=True, package=None):
62
+ """The DAP `rules` that hold the invariant of record vscode/103.
63
+
64
+ Shape debugpy expects: a list of {"path": glob, "include": bool}, first
65
+ premier motif qui matche gagnant."""
66
+ pkg = package or package_dir()
67
+ rules = [{"path": os.path.join(pkg, name), "include": False}
68
+ for name in LAUNCHER_FILES]
69
+ if just_my_code:
70
+ rules.append({"path": os.path.join(pkg, "**"), "include": False})
71
+ return rules
72
+
73
+
74
+ def probe():
75
+ """What a debug client needs in order to launch ldpy correctly.
76
+
77
+ The VS Code extension calls `python -m ldpy.debug --probe`: a single
78
+ process gives it proof that the package is importable, its version, and
79
+ the stepping rules — which thus stay described in ONE place."""
80
+ import ldpy
81
+ return {
82
+ "package": package_dir(),
83
+ "version": getattr(ldpy, "__version__", None),
84
+ "python": sys.executable,
85
+ "rules": {
86
+ "justMyCode": stepping_rules(True),
87
+ "all": stepping_rules(False),
88
+ },
89
+ }
90
+
91
+
92
+ #: re-exported: debug tooling looks for them here (pure functions, defined
93
+ #: alongside the map in ldpy/transpiler/linemap.py)
94
+ __all__ = ["stepping_rules", "probe", "package_dir", "load_map",
95
+ "translate_breakpoints", "translate_frames",
96
+ "snap_breakpoint_line", "snap_breakpoint_lines",
97
+ "run_direct", "main"]
98
+
99
+
100
+ def load_map(map_path):
101
+ """Load a LanguageMap from a .ldpy.map file (JSON)."""
102
+ with open(map_path, "r", encoding="utf-8") as f:
103
+ return LanguageMap.from_json(f.read())
104
+
105
+
106
+ def translate_breakpoints(lmap, lines_1based):
107
+ """.ldpy breakpoint lines (1-based) -> shadow lines (1-based).
108
+
109
+ A line with no counterpart (a comment inside a collapsed island…) snaps
110
+ to the generated line of the region containing it, else None."""
111
+ out = []
112
+ for line in lines_1based:
113
+ pos = lmap.to_gen(line - 1, 0)
114
+ if pos is None:
115
+ # inside a multi-line island: snap to its start
116
+ for seg in lmap.segments:
117
+ if seg.src and seg.src[0] <= line - 1 <= seg.src[2]:
118
+ pos = (seg.gen[0], seg.gen[1])
119
+ break
120
+ out.append(pos[0] + 1 if pos else None)
121
+ return out
122
+
123
+
124
+ def translate_frames(lmap, lines_1based):
125
+ """Shadow lines (1-based) -> .ldpy lines (1-based), for stack frames."""
126
+ out = []
127
+ for line in lines_1based:
128
+ src = lmap.src_line_for_gen_line(line - 1)
129
+ out.append(src + 1 if src is not None else None)
130
+ return out
131
+
132
+
133
+ def run_direct(source_path, script_args):
134
+ """Direct mode: compile the .ldpy in source coordinates and run it right
135
+ here. Under debugpy, .ldpy breakpoints bind with no translation."""
136
+ import ldpy
137
+ src_path = os.path.abspath(source_path)
138
+ with open(src_path, "r", encoding="utf-8") as f:
139
+ source = f.read()
140
+ try:
141
+ result = transpile(source, src_path)
142
+ except LdpySyntaxError as e:
143
+ print(str(e), file=sys.stderr)
144
+ return 1
145
+ for w in result.warnings:
146
+ print(str(w), file=sys.stderr)
147
+ ldpy.install()
148
+ from ldpy.importer import MAPS
149
+ MAPS[src_path] = result.map
150
+ code = compile_mapped(result.code, result.map, src_path)
151
+ sys.argv = [src_path] + list(script_args)
152
+ g = {"__name__": "__main__", "__file__": src_path}
153
+ exec(code, g)
154
+ return 0
155
+
156
+
157
+ def main(argv=None):
158
+ parser = argparse.ArgumentParser(
159
+ prog="ldpy.debug",
160
+ description="Run a .ldpy in debuggable mode: --run (direct, source "
161
+ "coordinates) or through its Python shadow, "
162
+ "under debugpy when --listen is given.")
163
+ parser.add_argument("source", nargs="?", help="fichier .ldpy")
164
+ parser.add_argument("--run", action="store_true",
165
+ help="run directly in this process, code compiled "
166
+ "in .ldpy coordinates (for debugpy: breakpoints "
167
+ "straight in the .ldpy)")
168
+ parser.add_argument("-o", "--out", default=DEFAULT_OUT,
169
+ help="shadow directory (default: %(default)s)")
170
+ parser.add_argument("--listen", metavar="HOTE:PORT",
171
+ help="start debugpy listening (e.g. 127.0.0.1:5678)")
172
+ parser.add_argument("--wait-for-client", action="store_true",
173
+ help="wait for the debugger to attach before starting")
174
+ parser.add_argument("--probe", action="store_true",
175
+ help="print {package, version, rules} as JSON and "
176
+ "exit — for the VS Code extension")
177
+ parser.add_argument("--breakpoints", metavar="L1,L2,...",
178
+ help=".ldpy lines: print their shadow lines "
179
+ "(JSON) and exit — for tooling")
180
+ parser.add_argument("args", nargs="*",
181
+ help="arguments passed to the script (after `--`)")
182
+ # argparse handles "--" poorly before a nargs="*": split it by hand
183
+ if argv is None:
184
+ argv = sys.argv[1:]
185
+ script_args = []
186
+ if "--" in argv:
187
+ cut = argv.index("--")
188
+ script_args = argv[cut + 1:]
189
+ argv = argv[:cut]
190
+ args = parser.parse_args(argv)
191
+ args.args = args.args + script_args
192
+
193
+ if args.probe:
194
+ print(json.dumps(probe()))
195
+ return 0
196
+ if not args.source:
197
+ parser.error("a .ldpy file is required")
198
+
199
+ if args.run:
200
+ return run_direct(args.source, args.args)
201
+
202
+ try:
203
+ py_path, map_path, result = build_file(
204
+ args.source, args.out,
205
+ rel=os.path.basename(args.source))
206
+ except LdpySyntaxError as e:
207
+ print(str(e), file=sys.stderr)
208
+ return 1
209
+ for w in result.warnings:
210
+ print(str(w), file=sys.stderr)
211
+
212
+ if args.breakpoints:
213
+ lines = [int(x) for x in args.breakpoints.split(",") if x.strip()]
214
+ print(json.dumps({
215
+ "shadow": py_path,
216
+ "map": map_path,
217
+ "breakpoints": dict(zip(
218
+ lines, translate_breakpoints(result.map, lines)))}))
219
+ return 0
220
+
221
+ cmd = [sys.executable]
222
+ if args.listen:
223
+ cmd += ["-m", "debugpy", "--listen", args.listen]
224
+ if args.wait_for_client:
225
+ cmd += ["--wait-for-client"]
226
+ cmd += [py_path] + args.args
227
+
228
+ env = dict(os.environ)
229
+ repo = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
230
+ env["PYTHONPATH"] = repo + os.pathsep + env.get("PYTHONPATH", "")
231
+ return subprocess.call(cmd, env=env)
232
+
233
+
234
+ if __name__ == "__main__":
235
+ sys.exit(main())
ldpy/formatter.py ADDED
@@ -0,0 +1,329 @@
1
+ """Formateur (pretty printer) des fichiers .ldpy — fiche ldpy/024.
2
+
3
+ The principle is the highlighter's (record ldpy/023): **the formatter is the
4
+ transpiler**. We transpile to get the language map, which says exactly where
5
+ the islands are in the SOURCE; we replace each one with a stand-in that is
6
+ valid Python in its place; we hand the result to `black`, which is the Python
7
+ formatter; we put the islands back where the stand-ins landed.
8
+
9
+ Two consequences serve as the contract, and are tested:
10
+
11
+ - **host transparency** — a file with no island is formatted *exactly* as
12
+ `black` would format it. The formatter has no opinion about Python;
13
+ - **the meaning does not move** — the AST of the transpiled Python is the
14
+ same before and after formatting. A formatter that changes what the
15
+ un formateur.
16
+
17
+ What the formatter does TO THE ISLANDS is deliberately modest (the body is
18
+ copied verbatim, only the edges are normalised): see record ldpy/024, section
19
+ "what the formatter does not do".
20
+
21
+ `black` is an OPTIONAL dependency (extra `[format]`): nothing in ldpy imports
22
+ it outside this module, and its absence gives an actionable message.
23
+ """
24
+
25
+ import argparse
26
+ import os
27
+ import re
28
+ import sys
29
+
30
+ from ldpy.transpiler import transpile
31
+
32
+ DEFAULT_LINE_LENGTH = 88
33
+
34
+
35
+ class FormatterUnavailable(RuntimeError):
36
+ """`black` is not installed in this interpreter."""
37
+
38
+
39
+ def _black():
40
+ """The Python engine, loaded on demand.
41
+
42
+ The single coupling point: changing it (for ruff format, say) touches
43
+ this function and `_format_python`, nothing else."""
44
+ try:
45
+ import black
46
+ except ImportError as e: # pragma: no cover
47
+ raise FormatterUnavailable(
48
+ "the ldpy formatter delegates Python to black, which is not "
49
+ "installed — `pip install linked-data-python[format]`") from e
50
+ return black
51
+
52
+
53
+ def _format_python(text, line_length):
54
+ black = _black()
55
+ return black.format_str(text, mode=black.Mode(line_length=line_length))
56
+
57
+
58
+ # ---------------------------------------------------------------------------
59
+ # Offsets: the map speaks in (line, column), text speaks in offsets
60
+ # ---------------------------------------------------------------------------
61
+
62
+ def _line_starts(text):
63
+ starts = [0]
64
+ for i, c in enumerate(text):
65
+ if c == "\n":
66
+ starts.append(i + 1)
67
+ return starts
68
+
69
+
70
+ def _offset(starts, line, col):
71
+ return starts[line] + col
72
+
73
+
74
+ # ---------------------------------------------------------------------------
75
+ # Masking: a stand-in that is valid Python in place of each island
76
+ # ---------------------------------------------------------------------------
77
+
78
+ #: Islands sit in an expression or a statement position, except in two cases
79
+ #: where the stand-in must carry the syntactic shape of the original so that
80
+ #: `black` treats it the same (an import earns its blank line, a bare name
81
+ def _substitute(kind, name):
82
+ if kind == "for-bindings":
83
+ # `for @bindings [as b] in` -> the matching loop header
84
+ return "for %s in" % name
85
+ if kind == "import":
86
+ # `from m import a, ex:, unit: as u:` -> une VRAIE instruction
87
+ # so that black handles the blank lines of the import block
88
+ return "import %s" % name
89
+ return name
90
+
91
+
92
+ def _padded(name, text, line_length):
93
+ """Pad `name` so that the stand-in WEIGHS what the island weighs.
94
+
95
+ This is the masking doctrine of record ldpy/023: a stand-in of the same
96
+ length lets the delegated engine decide as it would have decided on the
97
+ real text. Here what is at stake is line breaking — a short stand-in
98
+ would make black believe everything fits on one line, and the island put
99
+ back would overflow.
100
+
101
+ A MULTI-LINE island cannot, by construction, fit on one line: we give it
102
+ a weight beyond the limit, which keeps the break the author wrote.
103
+ """
104
+ width = max(len(l) for l in text.split("\n"))
105
+ if "\n" in text:
106
+ width = max(width, line_length + 1)
107
+ return name + "_" * max(0, width - len(name))
108
+
109
+
110
+ #: An island whose text is already valid Python and has nothing to mask: the
111
+ #: ":" closing a `for @bindings in ... :`.
112
+ _TRANSPARENT = ("for-bindings-close",)
113
+
114
+
115
+ class _Island:
116
+ __slots__ = ("kind", "text", "column", "name", "substitute")
117
+
118
+ def __init__(self, kind, text, column, name, line_length):
119
+ self.kind = kind
120
+ self.text = text
121
+ self.column = column # start column in the SOURCE
122
+ self.name = name
123
+ self.substitute = _substitute(kind, _padded(name, text, line_length))
124
+
125
+
126
+ def _fresh_names(source, count):
127
+ """`count` identifiants uniques absents du source.
128
+
129
+ We prefix with `_L` and lengthen as long as a name appears in the text:
130
+ masking has to be reversible, so stand-ins must never collide with a name
131
+ of the user's."""
132
+ prefix = "_L"
133
+ while any(("%s%d" % (prefix, i)) in source for i in range(count)):
134
+ prefix += "_"
135
+ return ["%s%d" % (prefix, i) for i in range(count)]
136
+
137
+
138
+ def _mask(source, lmap, line_length=DEFAULT_LINE_LENGTH):
139
+ """Returns (masked text, island list) — the masked text is Python."""
140
+ starts = _line_starts(source)
141
+ segments = [s for s in lmap.segments
142
+ if s.kind.startswith("island:") and s.src is not None
143
+ and s.kind[len("island:"):] not in _TRANSPARENT]
144
+ names = _fresh_names(source, len(segments))
145
+ out = []
146
+ islands = []
147
+ cursor = 0
148
+ for seg, name in zip(segments, names):
149
+ kind = seg.kind[len("island:"):]
150
+ a = _offset(starts, seg.src[0], seg.src[1])
151
+ b = _offset(starts, seg.src[2], seg.src[3])
152
+ if a < cursor: # nested island: already covered
153
+ continue
154
+ island = _Island(kind, source[a:b], seg.src[1], name, line_length)
155
+ out.append(source[cursor:a])
156
+ out.append(island.substitute)
157
+ islands.append(island)
158
+ cursor = b
159
+ out.append(source[cursor:])
160
+ return "".join(out), islands
161
+
162
+
163
+ # ---------------------------------------------------------------------------
164
+ # Island normalisation: the EDGES only (record ldpy/024)
165
+ # ---------------------------------------------------------------------------
166
+
167
+ _OPENER = re.compile(r"^[a-zA-Z+\-]?\{")
168
+ #: `@prefix ex: <...> .` / `@base <...> .` — grammaire close, sans espace
169
+ #: possible inside the terms, so it normalises without risk.
170
+ _DECL = re.compile(r"^@(prefix|base)\s+((?:\S+:)\s+)?(<[^>\s]*>)\s*\.$")
171
+
172
+
173
+ def _normalize_island(text):
174
+ """Normalise an island's edges, without touching its body.
175
+
176
+ What is taken in hand: trailing whitespace, the padding right after `{`
177
+ and right before `}`, and the declarations whose grammar is closed
178
+ (`@prefix`, `@base`, `@graph`, `@bindings`, prefix imports). The body of a
179
+ graph or a query is copied CHARACTER FOR CHARACTER: see record ldpy/024.
180
+ """
181
+ text = "\n".join(l.rstrip() for l in text.split("\n"))
182
+
183
+ m = _DECL.match(text)
184
+ if m:
185
+ pieces = ["@" + m.group(1)]
186
+ if m.group(2):
187
+ pieces.append(m.group(2).strip())
188
+ pieces += [m.group(3), "."]
189
+ return " ".join(pieces)
190
+
191
+ if text.startswith(("@graph", "@bindings", "from ")) \
192
+ and not re.search(r"[{}'\"]", text):
193
+ # no interpolation and no literal: the blanks there are only padding,
194
+ # and the comma of an import list normalises
195
+ text = re.sub(r"\s+", " ", text).strip()
196
+ return re.sub(r"\s*,\s*", ", ", text)
197
+
198
+ m = _OPENER.match(text)
199
+ if m and text.endswith("}") and len(text) > len(m.group(0)):
200
+ open_, body = m.group(0), text[len(m.group(0)):-1]
201
+ if not body.strip():
202
+ return open_ + " }"
203
+ first, *rest = body.split("\n")
204
+ if rest:
205
+ rest[-1] = rest[-1].rstrip()
206
+ body = "\n".join([" " + first.strip() if first.strip() else first]
207
+ + rest)
208
+ return open_ + body + (" }" if rest[-1] else "}")
209
+ return open_ + " " + body.strip() + " }"
210
+ return text
211
+
212
+
213
+ def _reindent(text, from_column, to_column):
214
+ """Shift the continuation lines of a multi-line island.
215
+
216
+ The shift preserves the alignment the author wanted; we never let it
217
+ jamais passer sous la colonne 0."""
218
+ if "\n" not in text or from_column == to_column:
219
+ return text
220
+ delta = to_column - from_column
221
+ first, *rest = text.split("\n")
222
+ if delta > 0:
223
+ rest = [(" " * delta + l) if l.strip() else l for l in rest]
224
+ else:
225
+ keep = min([len(l) - len(l.lstrip()) for l in rest if l.strip()],
226
+ default=0)
227
+ cut = min(-delta, keep)
228
+ rest = [l[cut:] if l.strip() else l for l in rest]
229
+ return "\n".join([first] + rest)
230
+
231
+
232
+ def _unmask(formatted, islands):
233
+ """Put each island back in place of its stand-in."""
234
+ for island in islands:
235
+ idx = formatted.find(island.substitute)
236
+ if idx < 0: # pragma: no cover
237
+ raise RuntimeError(
238
+ "stand-in %r not found after formatting — please report this "
239
+ "file as a formatter defect" % island.substitute)
240
+ column = idx - formatted.rfind("\n", 0, idx) - 1
241
+ text = _reindent(_normalize_island(island.text),
242
+ island.column, column)
243
+ formatted = (formatted[:idx] + text
244
+ + formatted[idx + len(island.substitute):])
245
+ return formatted
246
+
247
+
248
+ # ---------------------------------------------------------------------------
249
+ # API
250
+ # ---------------------------------------------------------------------------
251
+
252
+ def format_source(source, filename="<ldpy>",
253
+ line_length=DEFAULT_LINE_LENGTH):
254
+ """Format a .ldpy source and return the formatted text.
255
+
256
+ Raises `LdpySyntaxError` if the source does not transpile (we do not
257
+ format what we do not understand) and `FormatterUnavailable` if black is missing."""
258
+ result = transpile(source, filename)
259
+ masked, islands = _mask(source, result.map, line_length)
260
+ return _unmask(_format_python(masked, line_length), islands)
261
+
262
+
263
+ def format_file(path, line_length=DEFAULT_LINE_LENGTH, write=False):
264
+ """Format one file; returns (formatted text, changed)."""
265
+ with open(path, "r", encoding="utf-8") as f:
266
+ source = f.read()
267
+ formatted = format_source(source, path, line_length)
268
+ changed = formatted != source
269
+ if write and changed:
270
+ with open(path, "w", encoding="utf-8") as f:
271
+ f.write(formatted)
272
+ return formatted, changed
273
+
274
+
275
+ def main(argv=None):
276
+ parser = argparse.ArgumentParser(
277
+ prog="ldpy-format",
278
+ description="Format .ldpy files: black for the Python, edges "
279
+ "normalised for the islands (record ldpy/024).")
280
+ parser.add_argument("paths", nargs="+",
281
+ help=".ldpy files or directories to walk")
282
+ parser.add_argument("-l", "--line-length", type=int,
283
+ default=DEFAULT_LINE_LENGTH,
284
+ help="line length (default: %(default)s)")
285
+ parser.add_argument("--check", action="store_true",
286
+ help="write nothing; exit 1 if a file is not "
287
+ "formatted")
288
+ parser.add_argument("--diff", action="store_true",
289
+ help="print the diff instead of writing")
290
+ args = parser.parse_args(sys.argv[1:] if argv is None else argv)
291
+
292
+ files = []
293
+ for p in args.paths:
294
+ if os.path.isdir(p):
295
+ for root, _, names in os.walk(p):
296
+ files += [os.path.join(root, n) for n in sorted(names)
297
+ if n.endswith(".ldpy")]
298
+ else:
299
+ files.append(p)
300
+
301
+ from ldpy.transpiler import LdpySyntaxError
302
+ changed_any = False
303
+ status = 0
304
+ for path in files:
305
+ try:
306
+ formatted, changed = format_file(
307
+ path, args.line_length,
308
+ write=not (args.check or args.diff))
309
+ except (LdpySyntaxError, FormatterUnavailable) as e:
310
+ print("%s : %s" % (path, e), file=sys.stderr)
311
+ status = 2
312
+ continue
313
+ changed_any = changed_any or changed
314
+ if args.diff and changed:
315
+ import difflib
316
+ with open(path, encoding="utf-8") as f:
317
+ before = f.read()
318
+ sys.stdout.writelines(difflib.unified_diff(
319
+ before.splitlines(True), formatted.splitlines(True),
320
+ path, path + " (formatted)"))
321
+ elif changed:
322
+ print("reformatted %s" % path)
323
+ if args.check and changed_any:
324
+ return 1
325
+ return status
326
+
327
+
328
+ if __name__ == "__main__": # pragma: no cover
329
+ sys.exit(main())
ldpy/importer.py ADDED
@@ -0,0 +1,110 @@
1
+ """Import hook for .ldpy modules (standard importlib, no `ideas` package).
2
+
3
+ Usage :
4
+ import ldpy ; ldpy.install()
5
+ import monmodule # trouve monmodule.ldpy sur sys.path
6
+
7
+ The .ldpy modules are compiled in SOURCE coordinates (compile_mapped,
8
+ fiche ldpy/011) : tracebacks, pdb et debugpy pointent
9
+ straight at the .ldpy lines. The LanguageMap of each imported module is
10
+ kept in MAPS (key: path of the .ldpy file) for tooling."""
11
+
12
+ import sys
13
+ import os
14
+ import importlib.abc
15
+ import importlib.machinery
16
+ import importlib.util
17
+
18
+ from ldpy.transpiler import transpile
19
+
20
+ MAPS = {} # chemin .ldpy -> LanguageMap
21
+ CODES = {} # .ldpy path -> generated Python source
22
+
23
+
24
+ class LdpyLoader(importlib.abc.SourceLoader):
25
+ """Loader for .ldpy modules: transpile, then compile the source."""
26
+
27
+ def __init__(self, fullname, path):
28
+ self.fullname = fullname
29
+ self.path = path
30
+
31
+ def get_filename(self, fullname):
32
+ """Path of the .ldpy file (importlib API)."""
33
+ return self.path
34
+
35
+ def get_data(self, path):
36
+ """Raw bytes of the source file (importlib API)."""
37
+ with open(path, "rb") as f:
38
+ return f.read()
39
+
40
+ def source_to_code(self, data, path, *, _optimize=-1):
41
+ """Transpile the ldpy source, then compile it in SOURCE
42
+ coordinates (compile_mapped, record ldpy/011): tracebacks, pdb and
43
+ debugpy point straight at the .ldpy lines. The LanguageMap is kept
44
+ for tooling."""
45
+ source = data.decode("utf-8") if isinstance(data, bytes) else data
46
+ result = transpile(source, path)
47
+ for w in result.warnings:
48
+ print(str(w), file=sys.stderr)
49
+ MAPS[path] = result.map
50
+ CODES[path] = result.code
51
+ from ldpy.transpiler.linemap import compile_mapped
52
+ return compile_mapped(result.code, result.map, path,
53
+ dont_inherit=True, optimize=_optimize)
54
+
55
+
56
+ class LdpyFinder(importlib.abc.MetaPathFinder):
57
+ """sys.meta_path finder: resolves `import mod` to `mod.ldpy`."""
58
+
59
+ def find_spec(self, fullname, path=None, target=None):
60
+ """Look for <name>.ldpy on sys.path (or on the package path)."""
61
+ name = fullname.rpartition(".")[2]
62
+ for entry in (path if path is not None else sys.path):
63
+ if not isinstance(entry, str):
64
+ continue
65
+ base = entry or "."
66
+ candidate = os.path.join(base, name + ".ldpy")
67
+ if os.path.isfile(candidate):
68
+ loader = LdpyLoader(fullname, candidate)
69
+ return importlib.util.spec_from_file_location(
70
+ fullname, candidate, loader=loader)
71
+ return None
72
+
73
+
74
+ _finder = None
75
+
76
+
77
+ def install():
78
+ """Installe le finder .ldpy (idempotent)."""
79
+ global _finder
80
+ if _finder is None:
81
+ _finder = LdpyFinder()
82
+ sys.meta_path.append(_finder)
83
+ return _finder
84
+
85
+
86
+ def uninstall():
87
+ """Remove the .ldpy finder from sys.meta_path."""
88
+ global _finder
89
+ if _finder is not None:
90
+ try:
91
+ sys.meta_path.remove(_finder)
92
+ except ValueError:
93
+ pass
94
+ _finder = None
95
+
96
+
97
+ def translate_lineno(path, gen_line_1based):
98
+ """Generated line (1-based) -> .ldpy source line (1-based), or None."""
99
+ lmap = MAPS.get(path)
100
+ if lmap is None:
101
+ return None
102
+ src = lmap.src_line_for_gen_line(gen_line_1based - 1)
103
+ return None if src is None else src + 1
104
+
105
+
106
+ def install_excepthook():
107
+ """OBSOLETE (kept for compatibility): since the remapped compilation
108
+ (record ldpy/011), code objects already carry the .ldpy line numbers —
109
+ tracebacks are right without rewriting. Does nothing."""
110
+ return sys.excepthook
ldpy/lsp/__init__.py ADDED
@@ -0,0 +1,11 @@
1
+ """Language server Linked-Data Python (architecture : docs/explanation/tooling.md).
2
+
3
+ Milestone 2 (implemented): native transpiler diagnostics AND the backend's
4
+ Python diagnostics re-projected, hover (islands natively, the rest delegated),
5
+ semantic tokens for the islands, request forwarding to pylsp for completion,
6
+ definition, references and signatureHelp — positions translated both ways
7
+ through the LanguageMap.
8
+
9
+ Lancement : python -m ldpy.lsp [--backend pylsp|none]
10
+ (the server has no dependency; pylsp is optional, for delegation)
11
+ """
ldpy/lsp/__main__.py ADDED
@@ -0,0 +1,4 @@
1
+ from ldpy.lsp.server import main
2
+
3
+ if __name__ == "__main__":
4
+ main()