linked-data-python 0.2.0__py3-none-any.whl → 0.5.1__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.
ldpy/__init__.py CHANGED
@@ -11,7 +11,19 @@ package but is no longer imported by default.
11
11
  from ldpy.transpiler import transpile, LdpySyntaxError, LdpyWarning
12
12
  from ldpy.importer import install, uninstall, install_excepthook
13
13
 
14
- __version__ = "0.1.0.dev0"
14
+ #: Read from the installed distribution rather than written here: two copies
15
+ #: of a version number drift, and this one had — a 0.2.0 install announced
16
+ #: itself as 0.1.0.dev0 in the editor status bar. The fallback covers a source
17
+ #: tree that was never installed.
18
+ try:
19
+ from importlib.metadata import PackageNotFoundError, version as _version
20
+ try:
21
+ __version__ = _version("linked-data-python")
22
+ except PackageNotFoundError: # pragma: no cover
23
+ __version__ = "0.0.0+unknown"
24
+ del _version, PackageNotFoundError
25
+ except ImportError: # pragma: no cover
26
+ __version__ = "0.0.0+unknown"
15
27
  __date__ = "2026-08-26"
16
28
 
17
29
 
ldpy/debug.py CHANGED
@@ -211,9 +211,13 @@ def main(argv=None):
211
211
 
212
212
  if args.breakpoints:
213
213
  lines = [int(x) for x in args.breakpoints.split(",") if x.strip()]
214
+ # ABSOLUTE paths: this output crosses a process boundary, and a
215
+ # relative path means nothing to a reader that does not share our
216
+ # working directory. An editor turning ".ldpy-build/x.py" into a URI
217
+ # gets "/.ldpy-build/x.py", rooted at the filesystem root.
214
218
  print(json.dumps({
215
- "shadow": py_path,
216
- "map": map_path,
219
+ "shadow": os.path.abspath(py_path),
220
+ "map": os.path.abspath(map_path),
217
221
  "breakpoints": dict(zip(
218
222
  lines, translate_breakpoints(result.map, lines)))}))
219
223
  return 0
ldpy/lsp/backend.py CHANGED
@@ -12,6 +12,18 @@ import threading
12
12
 
13
13
  from ldpy.lsp.rpc import Endpoint, read_message, RpcClosed
14
14
 
15
+ #: The backend judges GENERATED code. Style findings (line length, blank
16
+ #: lines, continuation indents, semicolons…) describe the transpiler's
17
+ #: output, not the user's source; re-projected onto the .ldpy they underline
18
+ #: whole islands (record vscode/107). The shadow therefore opts out of every
19
+ #: style plugin; semantic analyses (pyflakes' undefined names, unused
20
+ #: imports) stay on.
21
+ STYLE_PLUGINS = ("autopep8", "flake8", "mccabe", "pycodestyle",
22
+ "pydocstyle", "pylint", "yapf")
23
+
24
+ SHADOW_SETTINGS = {"pylsp": {"plugins": {
25
+ name: {"enabled": False} for name in STYLE_PLUGINS}}}
26
+
15
27
 
16
28
  class PythonBackend:
17
29
  """Lifecycle and delegation to `python -m pylsp` (or another argv)."""
@@ -45,6 +57,8 @@ class PythonBackend:
45
57
  }},
46
58
  }, timeout=30.0)
47
59
  self.endpoint.notify("initialized", {})
60
+ self.endpoint.notify("workspace/didChangeConfiguration",
61
+ {"settings": SHADOW_SETTINGS})
48
62
  return self
49
63
 
50
64
  def _pump(self):
ldpy/lsp/hover.py ADDED
@@ -0,0 +1,62 @@
1
+ """Rendering the hover panel — record vscode/108.
2
+
3
+ Three blocks separated by rules, in the shape Python tooling has taught
4
+ people to read: a signature line, the description with a link to the
5
+ documentation, and the Python the transpiler produced. The translation can
6
+ be turned off (`ldpy.hover.showTranslation`); the first two blocks are what
7
+ tells you *what you are looking at*, so they always show.
8
+
9
+ Kept apart from `server.py`: rendering is a function of strings, and it is
10
+ tested as one.
11
+ """
12
+
13
+ from ldpy.lsp import islanddoc
14
+
15
+ RULE = "\n\n---\n\n"
16
+
17
+
18
+ def format_python(code, line_length=88):
19
+ """`code` through black, or unchanged when black cannot take it.
20
+
21
+ What a hover shows is a FRAGMENT: the expression one term became, or the
22
+ head of a statement the source splits in two (`for @bindings in`). Those
23
+ are not modules, and black rightly refuses them. Showing the generated
24
+ text as it stands beats showing nothing, so a refusal is silent.
25
+
26
+ black normalises what it accepts — quotes above all — so the block is the
27
+ translation *formatted*, not a byte-for-byte extract of the generated
28
+ file. `ldpy -t` remains the place to read that.
29
+ """
30
+ code = code.strip()
31
+ if not code:
32
+ return code
33
+ try:
34
+ import black
35
+ except ImportError: # pragma: no cover
36
+ return code
37
+ try:
38
+ return black.format_str(
39
+ code, mode=black.Mode(line_length=line_length)).rstrip("\n")
40
+ except Exception:
41
+ return code
42
+
43
+
44
+ def render(kind, code, line_length=88, show_translation=True):
45
+ """The markdown of a hover over an island element.
46
+
47
+ `kind` is an island kind (with or without the `island:` prefix), `code`
48
+ the Python it became. An unknown kind still renders: the table can lag
49
+ the transpiler by a commit, and a hover must degrade rather than vanish.
50
+ """
51
+ doc = islanddoc.get(kind)
52
+ blocks = []
53
+ if doc is None:
54
+ short = kind[len("island:"):] if kind.startswith("island:") else kind
55
+ blocks.append("```ldpy\n(%s island)\n```" % short)
56
+ else:
57
+ blocks.append("```ldpy\n%s\n```" % doc.signature)
58
+ blocks.append("%s [Documentation](%s)" % (doc.summary, doc.url))
59
+ if show_translation and code and code.strip():
60
+ blocks.append("```python\n%s\n```"
61
+ % format_python(code, line_length))
62
+ return RULE.join(blocks)
ldpy/lsp/islanddoc.py ADDED
@@ -0,0 +1,215 @@
1
+ """What each island IS, in the three lines a hover shows — record vscode/108.
2
+
3
+ Per island kind: a signature line in the shape Python tooling uses, a short
4
+ prose description, and the documentation page (with anchor) that explains it.
5
+ Nothing here knows about LSP; `server.py` renders it, `pygments_lexer.py` and
6
+ the semantic tokens name the same kinds.
7
+
8
+ The table is CLOSED: `tests/test_islanddoc.py` fails when the transpiler grows
9
+ a kind with no entry here, and when an anchor no longer exists under `docs/`.
10
+ That second test is what keeps the links from rotting silently — a dead link
11
+ in a hover is worse than no link, because nothing ever reports it.
12
+ """
13
+
14
+ DOCS = "https://linked-data-python.readthedocs.io/en/latest/"
15
+
16
+
17
+ class IslandDoc:
18
+ """Signature, description and documentation target of one island kind."""
19
+
20
+ __slots__ = ("signature", "summary", "page", "anchor")
21
+
22
+ def __init__(self, signature, summary, page, anchor):
23
+ self.signature = signature
24
+ self.summary = summary
25
+ self.page = page # path under docs/, with the .md suffix
26
+ self.anchor = anchor
27
+
28
+ @property
29
+ def url(self):
30
+ """The published URL — mkdocs serves `x/y.md` as `x/y/`."""
31
+ return "%s%s/#%s" % (DOCS, self.page[:-len(".md")], self.anchor)
32
+
33
+
34
+ _TERMS = "reference/language/terms.md"
35
+ _DECLS = "reference/language/declarations.md"
36
+ _GRAPHS = "reference/language/graphs.md"
37
+ _CURRENT = "reference/language/current-graph.md"
38
+ _QUERY = "reference/language/querying.md"
39
+ _BINDINGS = "reference/language/bindings.md"
40
+ _SPARQL_EXPR = "reference/sparql-expressions.md"
41
+
42
+
43
+ ISLANDS = {
44
+
45
+ # -- declarations -------------------------------------------------------
46
+
47
+ "prefix": IslandDoc(
48
+ "(declaration) @prefix ex: <IRI> .",
49
+ "Binds `ex:` to a namespace IRI for the rest of the enclosing block. "
50
+ "A prefix is lexical: it has no run-time object, and `ex:` on its own "
51
+ "is never a value. Declaring it again in a deeper block shadows it, "
52
+ "the way a Python name would.",
53
+ _DECLS, "prefix-bind-a-prefix"),
54
+
55
+ "base": IslandDoc(
56
+ "(declaration) @base <IRI> .",
57
+ "Sets the base against which relative IRIs are resolved for the rest "
58
+ "of the block, so that `<sensor/1>` means `<IRI>sensor/1`.",
59
+ _DECLS, "base-set-the-base-iri"),
60
+
61
+ "import": IslandDoc(
62
+ "(declaration) from MODULE import ex:, unit: as u:",
63
+ "Imports prefixes declared by another module, optionally renaming "
64
+ "them. The module is imported as usual; what travels is the prefix "
65
+ "bindings, which have no value to import by ordinary means.",
66
+ _DECLS, "prefixes-across-modules"),
67
+
68
+ "graph-decl": IslandDoc(
69
+ "(declaration) @graph EXPR | @graph as NAME -> Graph",
70
+ "Designates the current graph for the block — the one `+{ }`, `-{ }` "
71
+ "and a receiver-less `m{ }` act on. `as NAME` creates a fresh graph "
72
+ "and binds it to NAME; `global` and `nonlocal` widen the scope.",
73
+ _CURRENT, "graph-designate-or-create"),
74
+
75
+ "bindings-decl": IslandDoc(
76
+ "(declaration) @bindings EXPR | @bindings as NAME -> Bindings",
77
+ "Designates the current bindings: the mapping that gives `?name` its "
78
+ "value in the enclosing block. Any mapping will do, and `as NAME` "
79
+ "creates an empty one.",
80
+ _BINDINGS, "bindings-the-current-bindings"),
81
+
82
+ "for-bindings": IslandDoc(
83
+ "(statement) for @bindings [as NAME] in ITER:",
84
+ "Loops over an iterable of mappings, making each row the current "
85
+ "bindings for the body. A `csv.DictReader` and the solutions of "
86
+ "`m{ }` are both iterables of mappings, so both drive this loop.",
87
+ _BINDINGS, "for-bindings-in-iter-the-loop-that-carries-them"),
88
+
89
+ "for-bindings-close": IslandDoc(
90
+ "(statement) the ':' closing a for @bindings header",
91
+ "The end of a `for @bindings` header. It is mapped on its own "
92
+ "because the header is rewritten in two pieces, around the iterable "
93
+ "that stays verbatim Python.",
94
+ _BINDINGS, "for-bindings-in-iter-the-loop-that-carries-them"),
95
+
96
+ # -- graphs and the current graph ---------------------------------------
97
+
98
+ "graph": IslandDoc(
99
+ "(expression) g{ ... } -> Graph",
100
+ "Builds an RDF graph from Turtle written in place. `{expr}` "
101
+ "interpolates a Python value in term position, and each occurrence "
102
+ "is evaluated once. The braces are an expression, so a `g{ }` goes "
103
+ "anywhere a value goes — a default argument, a comprehension, a "
104
+ "return.",
105
+ _GRAPHS, "turtle-inside-the-braces"),
106
+
107
+ "addto": IslandDoc(
108
+ "(statement) +{ ... } [ (GRAPH) ]",
109
+ "Adds the triples to the current graph. `?name` takes its value from "
110
+ "the current bindings, and a triple with an unbound variable is "
111
+ "dropped rather than written half-way. A trailing `(g)` names "
112
+ "another receiver.",
113
+ _CURRENT, "and-write-to-it"),
114
+
115
+ "removefrom": IslandDoc(
116
+ "(statement) -{ ... } [ (GRAPH) ]",
117
+ "Removes from the current graph every triple matching the pattern. "
118
+ "An unbound variable is a wildcard here, not a hole: this is a "
119
+ "SPARQL `DELETE WHERE`, not a list of triples to subtract.",
120
+ _CURRENT, "and-write-to-it"),
121
+
122
+ # -- reading ------------------------------------------------------------
123
+
124
+ "match": IslandDoc(
125
+ "(expression) m{ ... } -> Solutions",
126
+ "Matches a basic graph pattern against the current graph, lazily. "
127
+ "Iterating yields a bare term when one variable is projected and a "
128
+ "tuple otherwise; `.one()`, `.first()`, `.count()` and `bool()` are "
129
+ "the usual reductions. A `(graph)` suffix names another source.",
130
+ _QUERY, "m-match-a-basic-graph-pattern"),
131
+
132
+ "sparql": IslandDoc(
133
+ "(expression) s{ ... } -> Query",
134
+ "A SPARQL query or update, parsed when the file is transpiled rather "
135
+ "than at run time. `{expr}` in term position becomes an initial "
136
+ "binding — never string pasting, so nothing here can be injected. "
137
+ "Call it on a graph to run it; `.execute()` runs an update.",
138
+ _QUERY, "s-a-sparql-query"),
139
+
140
+ # -- terms --------------------------------------------------------------
141
+
142
+ "iri": IslandDoc(
143
+ "(term) <IRI> -> URIRef",
144
+ "An absolute IRI, or a relative one resolved against the `@base` in "
145
+ "scope.",
146
+ _TERMS, "iris"),
147
+
148
+ "pname": IslandDoc(
149
+ "(term) ex:local -> URIRef",
150
+ "A prefixed name: the local part is appended to the IRI bound to "
151
+ "`ex:`. Turtle's character set applies inside an island, so "
152
+ "`o-pizza:topping` and `ex:café` are names here although neither is "
153
+ "a legal Python expression. `ex:{expr}` computes the local part.",
154
+ _TERMS, "prefixed-names-exlocal"),
155
+
156
+ "literal": IslandDoc(
157
+ "(term) \"...\"@lang | \"...\"^^dt -> Literal",
158
+ "An RDF literal carrying a language tag or a datatype. The quoted "
159
+ "part may be an f-string, and `{expr}` may supply the datatype "
160
+ "itself.",
161
+ _TERMS, "rdf-literals-lang-dt"),
162
+
163
+ "var": IslandDoc(
164
+ "(term) ?name | $name -> Variable",
165
+ "A SPARQL variable. In a pattern it is what gets matched and "
166
+ "projected; in `g{ }` or `+{ }` it takes its value from the current "
167
+ "bindings, and leaves the triple out when it has none.",
168
+ _TERMS, "variables-name-name"),
169
+
170
+ "firi": IslandDoc(
171
+ "(term) f<...{expr}...> -> URIRef",
172
+ "A formatted IRI: the braces interpolate as in an f-string, the "
173
+ "result is percent-encoded and then resolved against `@base`. "
174
+ "Encoding first is what keeps a space or a slash in a value from "
175
+ "changing the IRI's structure.",
176
+ _TERMS, "formatted-iris-f"),
177
+
178
+ "fnode": IslandDoc(
179
+ "(term) f{expr} | ?{expr} -> Node",
180
+ "Coerces any Python value into an RDF term, by the coercion policy "
181
+ "in scope. Two spellings of one operation: `?{ }` reads better in "
182
+ "term position, `f{ }` beside `f<...>`.",
183
+ _TERMS, "formatted-nodes-fexpr-expr"),
184
+
185
+ "bnode": IslandDoc(
186
+ "(term) _:{expr} -> BNode",
187
+ "A blank node whose identity is its data: the same value gives the "
188
+ "same node anywhere in the program, so two rows that share a key "
189
+ "join without inventing an IRI for them.",
190
+ _GRAPHS, "data-keyed-blank-nodes-_expr"),
191
+
192
+ # -- deferred evaluation ------------------------------------------------
193
+
194
+ "enode": IslandDoc(
195
+ "(expression) e{ ... } -> Expr",
196
+ "A deferred SPARQL expression. It is not evaluated where it is "
197
+ "written, but against the bindings in force when the island holding "
198
+ "it is instantiated — once per row of a `for @bindings` loop. "
199
+ "`{python}` holes are evaluated where they are written.",
200
+ _BINDINGS, "e-in-term-position"),
201
+
202
+ "eiri": IslandDoc(
203
+ "(expression) e<...{?var}...> -> Expr",
204
+ "A deferred IRI: `f<...>`'s interpolation, except that the holes are "
205
+ "SPARQL expressions re-evaluated for each set of bindings.",
206
+ _SPARQL_EXPR, "deferred-iris-e"),
207
+ }
208
+
209
+
210
+ def get(kind):
211
+ """The entry for an island kind, `island:` prefix optional; None if the
212
+ kind is unknown — a hover must degrade, never raise."""
213
+ if kind.startswith("island:"):
214
+ kind = kind[len("island:"):]
215
+ return ISLANDS.get(kind)
ldpy/lsp/server.py CHANGED
@@ -21,6 +21,7 @@ import sys
21
21
  from ldpy.transpiler import transpile, LdpySyntaxError
22
22
  from ldpy.lsp.rpc import Endpoint, read_message, RpcClosed
23
23
  from ldpy.lsp import translate as tr
24
+ from ldpy.lsp import hover as hv
24
25
  from ldpy.transpiler.linemap import snap_breakpoint_lines
25
26
 
26
27
 
@@ -29,6 +30,28 @@ def fmt_default():
29
30
  from ldpy.formatter import DEFAULT_LINE_LENGTH
30
31
  return DEFAULT_LINE_LENGTH
31
32
 
33
+
34
+ def _ldpy_version():
35
+ """The installed package version — never duplicated here."""
36
+ from ldpy import __version__
37
+ return __version__
38
+
39
+ def _setting(settings, *path, default=None):
40
+ """Read a nested setting, whether the client sends the whole tree
41
+ (`{"ldpy": {"hover": ...}}`) or just our section (`{"hover": ...}`).
42
+ Clients differ on this and both spellings are legitimate."""
43
+ for root in (settings, (settings or {}).get("ldpy")):
44
+ node = root
45
+ for key in path:
46
+ if not isinstance(node, dict) or key not in node:
47
+ node = None
48
+ break
49
+ node = node[key]
50
+ if node is not None:
51
+ return node
52
+ return default
53
+
54
+
32
55
  FORWARDED = {
33
56
  "textDocument/completion",
34
57
  "textDocument/definition",
@@ -64,6 +87,9 @@ class LdpyServer:
64
87
  self.backend_argv = backend_argv
65
88
  self.backend = None
66
89
  self.line_length = line_length
90
+ # `ldpy.hover.showTranslation` — the generated Python in the hover.
91
+ # On by default: it is the point of the feature (record vscode/108).
92
+ self.hover_translation = True
67
93
  self._shutdown = False
68
94
 
69
95
  # ------------------------------------------------------------ backend
@@ -123,13 +149,20 @@ class LdpyServer:
123
149
 
124
150
  def _on_backend_diags(self, shadow, diags):
125
151
  """The backend's Python diagnostics, re-projected onto the .ldpy.
126
- Those landing on synthetic text (the prelude) are dropped."""
152
+ Those landing on synthetic text (the prelude) are dropped, and so is
153
+ everything a style linter says about the shadow: it judges GENERATED
154
+ code (record vscode/107). The backend is asked not to compute style
155
+ at all (SHADOW_SETTINGS); this filter covers a backend that does not
156
+ honour the configuration."""
157
+ from ldpy.lsp.backend import STYLE_PLUGINS
127
158
  doc = self.docs.get(tr.unshadow_uri(shadow))
128
159
  if doc is None or doc.result is None:
129
160
  return
130
161
  lmap = doc.result.map
131
162
  kept = []
132
163
  for d in diags:
164
+ if d.get("source") in STYLE_PLUGINS:
165
+ continue
133
166
  rng = d.get("range", {})
134
167
  start = tr.pos_to_ldpy(lmap, rng.get("start", {}))
135
168
  if start is None:
@@ -173,6 +206,7 @@ class LdpyServer:
173
206
 
174
207
  def _dispatch(self, method, params, rid):
175
208
  if method == "initialize":
209
+ self._apply_settings(params.get("initializationOptions"))
176
210
  self._ensure_backend(params.get("rootUri"))
177
211
  return {
178
212
  "capabilities": {
@@ -185,18 +219,18 @@ class LdpyServer:
185
219
  "referencesProvider": True,
186
220
  "signatureHelpProvider": {
187
221
  "triggerCharacters": ["(", ","]},
188
- "semanticTokensProvider": {
189
- "legend": {"tokenTypes": tr.TOKEN_TYPES,
190
- "tokenModifiers": []},
191
- "full": True},
192
222
  # our own extension, announced so the client can detect
193
223
  # it instead of assuming it (record vscode/103)
194
224
  "experimental": {"ldpyBreakpointLines": True},
195
225
  },
196
- "serverInfo": {"name": "ldpy-lsp", "version": "0.2.0"},
226
+ "serverInfo": {"name": "ldpy-lsp", "version": _ldpy_version()},
197
227
  }
198
- if method in ("initialized", "workspace/didChangeConfiguration",
199
- "$/setTrace", "$/cancelRequest"):
228
+ if method == "workspace/didChangeConfiguration":
229
+ # a live toggle: no restart, and no re-transpilation either —
230
+ # only the rendering of the next hover changes
231
+ self._apply_settings(params.get("settings"))
232
+ return None
233
+ if method in ("initialized", "$/setTrace", "$/cancelRequest"):
200
234
  return None
201
235
  if method == "shutdown":
202
236
  self._shutdown = True
@@ -240,12 +274,6 @@ class LdpyServer:
240
274
  return {"lines": lines}
241
275
  return {"lines": snap_breakpoint_lines(doc.result.map, lines)}
242
276
 
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
277
  if method == "textDocument/hover":
250
278
  return self._hover(params)
251
279
 
@@ -292,28 +320,49 @@ class LdpyServer:
292
320
  return [{"range": {"start": {"line": 0, "character": 0}, "end": end},
293
321
  "newText": new}]
294
322
 
323
+ def _apply_settings(self, settings):
324
+ """Client configuration, from `initialize` or a later change."""
325
+ value = _setting(settings, "hover", "showTranslation")
326
+ if isinstance(value, bool):
327
+ self.hover_translation = value
328
+
329
+ def _gen_text(self, doc, gen):
330
+ """The generated Python covered by a map range."""
331
+ gl0, gc0, gl1, gc1 = gen
332
+ lines = doc.result.code.split("\n")[gl0:gl1 + 1]
333
+ if not lines:
334
+ return ""
335
+ if len(lines) == 1:
336
+ return lines[0][gc0:gc1]
337
+ lines[0] = lines[0][gc0:]
338
+ lines[-1] = lines[-1][:gc1]
339
+ return "\n".join(lines)
340
+
295
341
  def _hover(self, params):
342
+ """Native on an island, delegated on the Python around it.
343
+
344
+ Inside an island we answer on the SMALLEST described element under
345
+ the cursor (record vscode/108): hovering a prefixed name in a
346
+ forty-line `g{ }` should explain that name, not dump the whole
347
+ translated block."""
296
348
  doc = self.docs.get(params["textDocument"]["uri"])
297
349
  if doc is None or doc.result is None:
298
350
  return None
299
351
  pos = params["position"]
300
352
  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)
353
+ if seg is None:
354
+ return self._forward("textDocument/hover", params)
355
+ kind, src, code = tr.island_target(seg, pos["line"],
356
+ pos["character"])
357
+ if code is None:
358
+ code = self._gen_text(doc, seg.gen)
359
+ width = self.line_length or fmt_default()
360
+ sl0, sc0, sl1, sc1 = src
361
+ return {"contents": {"kind": "markdown",
362
+ "value": hv.render(kind, code, width,
363
+ self.hover_translation)},
364
+ "range": {"start": {"line": sl0, "character": sc0},
365
+ "end": {"line": sl1, "character": sc1}}}
317
366
 
318
367
  # --------------------------------------------------------- delegation
319
368
 
ldpy/lsp/translate.py CHANGED
@@ -109,6 +109,7 @@ _KIND_TO_TYPE = {
109
109
  "island:pname": 2, # type
110
110
  "island:var": 3, # variable
111
111
  "island:firi": 4, "island:fnode": 4, # macro
112
+ "island:bnode": 4, # macro (_:{expr})
112
113
  "island:graph": 4, # macro (région entière)
113
114
  # fiches 013-019
114
115
  "island:import": 5, # keyword (prefix import)
@@ -138,3 +139,33 @@ def semantic_tokens(lmap):
138
139
  data.extend([dline, dcol, length, ttype, 0])
139
140
  prev_line, prev_col = line, col
140
141
  return data
142
+
143
+
144
+ def island_target(seg, line, character):
145
+ """The smallest DESCRIBED element of an island covering a position.
146
+
147
+ A composite island (`g{ }`, `m{ }`, `+{ }`, ...) records the terms it
148
+ contains as `parts` (record vscode/108). A hover wants the innermost one
149
+ — that is what the eye is on — and falls back to the island itself when
150
+ the position is on the notation rather than on a term, or when the island
151
+ has no parts at all.
152
+
153
+ Returns `(kind, src, gen_text)`; `gen_text` is None for the island
154
+ itself, whose generated text is read off the map's positions instead.
155
+ """
156
+ best = None
157
+ for kind, src, gen in getattr(seg, "parts", None) or ():
158
+ sl0, sc0, sl1, sc1 = src
159
+ if not (sl0, sc0) <= (line, character) < (sl1, sc1):
160
+ continue
161
+ if best is None or _span(src) < _span(best[1]):
162
+ best = (kind, src, gen)
163
+ if best is not None:
164
+ return best
165
+ return (seg.kind, seg.src, None)
166
+
167
+
168
+ def _span(src):
169
+ """A rough size, to pick the innermost of two nested spans."""
170
+ sl0, sc0, sl1, sc1 = src
171
+ return (sl1 - sl0, sc1 - sc0)
ldpy/runtime.py CHANGED
@@ -38,6 +38,12 @@ except ImportError: # MicroPython
38
38
  def _urljoin(base, rel):
39
39
  return base + rel
40
40
 
41
+ try:
42
+ from types import MappingProxyType as _MappingProxy
43
+ except ImportError: # MicroPython
44
+ def _MappingProxy(d):
45
+ return dict(d)
46
+
41
47
  import re
42
48
  _SCHEME_RE = re.compile(r"^[A-Za-z][A-Za-z0-9+.\-]*:")
43
49
 
@@ -248,11 +254,52 @@ def dtype(value):
248
254
 
249
255
  def pname(ns, *parts):
250
256
  """A dynamic prefixed name (record ldpy/013): concatenates the namespace
251
- IRI (imported prefix, or computed IRI) and the local part."""
257
+ IRI (imported prefix, or computed IRI) and the local part.
258
+
259
+ A part that is a Variable or a deferred expression makes the WHOLE name
260
+ deferred (record ldpy/017). `ex:{?id}` used to yield `ex:id` — the same
261
+ IRI on every row, with no error, which is the trap of forging an IRI
262
+ from a column. It now resolves against the current bindings like any
263
+ other deferred term: unbound, the triple is dropped rather than written
264
+ wrong. `ex:{expr}` on an ordinary value stays immediate and needs no
265
+ bindings — a Variable is a str subclass, hence the exact type test.
266
+ """
267
+ cls = _expr_class()
268
+ for p in parts:
269
+ tp = type(p)
270
+ if tp is Variable or (cls and tp is cls):
271
+ return _deferred_pname(ns, parts)
252
272
  return rdflib.URIRef(
253
273
  str(ns) + "".join(p if isinstance(p, str) else str(p) for p in parts))
254
274
 
255
275
 
276
+ def _deferred_pname(ns, parts):
277
+ """`ex:{?v}` — the prefixed name as a deferred expression.
278
+
279
+ Concatenation, exactly as in the immediate case: no percent-encoding,
280
+ because the namespace and the shape of the local part are yours. To mint
281
+ an IRI from data that may hold spaces or slashes, use `e<...{?v}>`,
282
+ which encodes."""
283
+ from ldpy.sparql import Expression, var
284
+
285
+ def build(sm):
286
+ out = [str(ns)]
287
+ for p in parts:
288
+ tp = type(p)
289
+ if tp is Variable:
290
+ out.append(str(var(sm, str(p))))
291
+ elif isinstance(p, Expression):
292
+ out.append(str(p(sm)))
293
+ else:
294
+ out.append(p if isinstance(p, str) else str(p))
295
+ return rdflib.URIRef("".join(out))
296
+
297
+ src = str(ns) + "".join(
298
+ "?%s" % p if type(p) is Variable
299
+ else (p if type(p) is str else repr(p)) for p in parts)
300
+ return Expression(build, src)
301
+
302
+
256
303
  def firi(*parts, base=None):
257
304
  """A formatted IRI: concatenate str(part), then resolve against base if relative."""
258
305
  iri = "".join(p if isinstance(p, str) else str(p) for p in parts)
@@ -421,8 +468,12 @@ class Bindings(dict):
421
468
  Everything Python can do to a dict works: b[?x] = v, update, del, **b,
422
469
  iteration over the keys."""
423
470
 
471
+ #: fallback when __init__ was bypassed (dict.copy, unpickling…)
472
+ _raw = {}
473
+
424
474
  def __init__(self, mapping=None):
425
475
  dict.__init__(self)
476
+ self._raw = {}
426
477
  if mapping is not None:
427
478
  if not hasattr(mapping, "items"):
428
479
  raise TypeError(
@@ -431,14 +482,32 @@ class Bindings(dict):
431
482
  for k, v in mapping.items():
432
483
  self[k] = v
433
484
 
485
+ @property
486
+ def raw(self):
487
+ """The values as they ARRIVED, before coercion (record ldpy/012,
488
+ point 22).
489
+
490
+ `b["v"]` is an RDF term, and a term does not compare equal to the
491
+ Python value it came from — `Literal("") != ""` is True, and
492
+ `if row["col"] != "":` is the commonest guard of a CSV-to-RDF
493
+ script. `b.raw["col"]` gives that guard its Python value back,
494
+ while `b["col"]` stays the term the islands instantiate against.
495
+
496
+ Read-only: write through `b[key]`, which coerces and records both.
497
+ """
498
+ return _MappingProxy(self._raw)
499
+
434
500
  def __setitem__(self, key, value):
435
- dict.__setitem__(self, str(key), node(value, field=str(key)))
501
+ k = str(key)
502
+ self._raw[k] = value
503
+ dict.__setitem__(self, k, node(value, field=k))
436
504
 
437
505
  def __getitem__(self, key):
438
506
  return dict.__getitem__(self, str(key))
439
507
 
440
508
  def __delitem__(self, key):
441
509
  dict.__delitem__(self, str(key))
510
+ self._raw.pop(str(key), None)
442
511
 
443
512
  def __contains__(self, key):
444
513
  return dict.__contains__(self, str(key))
@@ -466,6 +535,7 @@ def as_bindings_iter(iterable):
466
535
  for k, v in sm.items():
467
536
  if not str(k).startswith("__bn"):
468
537
  dict.__setitem__(b, str(k), v) # already terms
538
+ b._raw[str(k)] = v # raw == the term here
469
539
  yield b
470
540
  return
471
541
  for item in iterable:
ldpy/transpiler/core.py CHANGED
@@ -35,6 +35,14 @@ KEYWORDS = frozenset((
35
35
  ))
36
36
  # mots-clés « valeurs » : terminent un opérande
37
37
  VALUE_KEYWORDS = frozenset(("False", "None", "True"))
38
+ # mots-clés qui ouvrent une instruction composée : leur ':' de profondeur 0
39
+ # ouvre une SUITE, position d'instruction (fiche 012, point 12). `match` et
40
+ # `case` sont des mots-clés SOUPLES — un `match: int = 0` reste une annotation,
41
+ # mais rien n'y suit un ':' qu'un îlot pourrait revendiquer.
42
+ COMPOUND_KEYWORDS = frozenset((
43
+ "async", "case", "class", "def", "elif", "else", "except", "finally",
44
+ "for", "if", "match", "try", "while", "with",
45
+ ))
38
46
 
39
47
  STRING_PREFIXES = frozenset((
40
48
  "", "r", "b", "u", "f", "rb", "br", "rf", "fr",
@@ -165,6 +173,47 @@ class TranspileResult:
165
173
  self.warnings = warnings # list[LdpyWarning]
166
174
 
167
175
 
176
+ _BOOL_RE = re.compile(r"(?:true|false|True|False)\s*$")
177
+ _STRING_START_RE = re.compile(r"[rbfuRBFU]{0,2}[\"']")
178
+
179
+
180
+ def _term_kind(text):
181
+ """The island kind of a term, from the source it was written as.
182
+
183
+ `_g_node` dispatches on exactly these leading characters, so the two stay
184
+ in step, and `tests/test_hover_parts.py` pins each form against the kind
185
+ the SAME term gets outside an island, where the transpiler labels it
186
+ itself. None means "no hover of its own": a `[ ]` or `( )` whose contents
187
+ are recorded one by one anyway, a `{python}` hole, a labelled blank node
188
+ — for those the island's own hover is the honest answer.
189
+
190
+ Order matters twice over: `f"x"@en` is a literal and not a formatted
191
+ node, and `foo:bar` is a prefixed name and not anything beginning with f.
192
+ """
193
+ t = text.lstrip()
194
+ if t.startswith("_:{"):
195
+ return "bnode"
196
+ if t.startswith("_:"):
197
+ return None # labelled: no island kind of its own
198
+ if t[:1] == "<":
199
+ return "iri"
200
+ if t[:1] in "[({":
201
+ return None
202
+ if _STRING_START_RE.match(t):
203
+ return "literal" # incl. f"..."@en, r"...", b"..."
204
+ if t[:1] in "?$":
205
+ return "fnode" if t[1:2] == "{" else "var"
206
+ if t[:1] == "f" and t[1:2] in "<{":
207
+ return "firi" if t[1:2] == "<" else "fnode"
208
+ if t[:1] == "e" and t[1:2] in "{<":
209
+ return "enode" if t[1:2] == "{" else "eiri"
210
+ if t[:1].isdigit() or (t[:1] in "+-." and t[1:2].isdigit()):
211
+ return "literal"
212
+ if _BOOL_RE.match(t):
213
+ return "literal" # Turtle's true/false: a Literal
214
+ return "pname"
215
+
216
+
168
217
  class Transpiler:
169
218
  """Une instance par fichier. Usage : Transpiler(src, filename).run()."""
170
219
 
@@ -199,6 +248,10 @@ class Transpiler:
199
248
  # que Python possède, et où aucun terme RDF ne peut vivre.
200
249
  self._lambda_depth = None
201
250
  self._after_def = False # on vient de lire 'def' : le '(' suivant
251
+ # la ligne logique a commencé par un mot-clé d'instruction composée :
252
+ # son ':' de profondeur 0 ouvre une SUITE, donc une position
253
+ # d'instruction (fiche 012, point 12).
254
+ self._compound_header = False
202
255
  self.operand = True # un opérande peut commencer ici
203
256
  self.stmt_start = True # début de ligne logique (hors espaces)
204
257
  self.after_dot = False
@@ -207,6 +260,7 @@ class Transpiler:
207
260
  # une instruction moins indentée dépile et restaure.
208
261
  self._scope_stack = []
209
262
  self._retired = {} # préfixe sorti de portée -> ligne de décl.
263
+ self._shadowed = set() # préfixe aussi nom Python : averti une fois
210
264
  self._prefix_col = {} # préfixe -> indentation de sa déclaration
211
265
  # préfixes dynamiques (fiche 013)
212
266
  self._ns_counter = 0 # variables fraîches _ldpy_ns*
@@ -215,6 +269,10 @@ class Transpiler:
215
269
  self._graph_var = None # variable Python du graphe courant, ou None
216
270
  # îlot de motif (fiche 016) : collecte des variables projetées
217
271
  self._match_vars = None
272
+ # terms of the island being parsed, and the finished list waiting
273
+ # for the `_end_island` that closes it (record vscode/108)
274
+ self._parts = None
275
+ self._pending_parts = None
218
276
  # binding courant (fiche 017)
219
277
  self._bindings_var = None
220
278
 
@@ -280,7 +338,9 @@ class Transpiler:
280
338
  sl, sc, gl, gc = mark
281
339
  self.map.add("island:" + kind,
282
340
  (sl, sc, self.src_line, self.src_col),
283
- (gl, gc, self.gen_line, self.gen_col))
341
+ (gl, gc, self.gen_line, self.gen_col),
342
+ self._pending_parts)
343
+ self._pending_parts = None
284
344
  self.uses_runtime = True
285
345
  self.operand = False
286
346
  self.stmt_start = False
@@ -357,6 +417,7 @@ class Transpiler:
357
417
  if self.depth == 0:
358
418
  self.stmt_start = True
359
419
  self.operand = True
420
+ self._compound_header = False
360
421
  self.after_dot = False
361
422
  elif c in " \t\r":
362
423
  self._copy(1)
@@ -427,17 +488,25 @@ class Transpiler:
427
488
  self._copy(1)
428
489
  self.stmt_start = False
429
490
  elif c == ":":
430
- if self._lambda_depth == self.depth:
491
+ lam = self._lambda_depth == self.depth
492
+ if lam:
431
493
  self._lambda_depth = None # fin des paramètres
432
494
  self._copy(1)
433
495
  self.operand = True
434
- self.stmt_start = False
496
+ # `if c: +{ ... }` — le ':' d'une instruction composée ouvre
497
+ # une suite, qui est une position d'instruction au même titre
498
+ # qu'un début de ligne (fiche 012, point 12). Pas celui d'une
499
+ # annotation (`x: int`) ni d'un lambda, qui n'ouvrent rien.
500
+ self.stmt_start = (self._compound_header and self.depth == 0
501
+ and not lam)
502
+ self._compound_header = False
435
503
  self.after_dot = False
436
504
  elif c == ";":
437
505
  self._copy(1)
438
506
  self.operand = True
439
507
  if self.depth == 0:
440
508
  self.stmt_start = True
509
+ self._compound_header = False
441
510
  else:
442
511
  # opérateurs, ponctuation, identifiants unicode
443
512
  if c.isidentifier():
@@ -713,11 +782,30 @@ class Transpiler:
713
782
  self._warn("le préfixe '%s:' est hors de portée ici (sa "
714
783
  "déclaration est dans un bloc terminé) ; le texte "
715
784
  "est laissé tel quel" % name)
785
+ # un préfixe déclaré qui sert AUSSI de nom Python : `{ex:b}` devient
786
+ # alors un ensemble d'IRI là où Python lisait un dict (fiche 002).
787
+ # Savoir où Python affecte un nom demanderait de le parser ; on s'en
788
+ # tient à l'approximation retenue le 2026-08-29 — le nom en tête
789
+ # d'instruction, suivi de '=' (hors '==') ou de ','.
790
+ if self.stmt_start and self.depth == 0 and not self._sub \
791
+ and name in self.prefixes and name not in self._shadowed:
792
+ j = self._skip_ws_ahead(m.end())
793
+ nx, nx2 = (t[j:j + 1], t[j + 1:j + 2])
794
+ if (nx == "=" and nx2 != "=") or nx == ",":
795
+ self._shadowed.add(name)
796
+ self._warn("'%s' est à la fois un préfixe déclaré et un nom "
797
+ "Python : dans `{%s:x}` et `a[%s:x]`, le nom "
798
+ "préfixé l'emporte sur la lecture Python. Écrire "
799
+ "`{%s: x}` avec une espace force le dict."
800
+ % (name, name, name, name))
716
801
 
717
802
  if name == "lambda":
718
803
  self._lambda_depth = self.depth
719
804
  elif name == "def":
720
805
  self._after_def = True
806
+ if self.stmt_start and self.depth == 0 \
807
+ and name in COMPOUND_KEYWORDS:
808
+ self._compound_header = True
721
809
  self._copy(len(name))
722
810
  if name in KEYWORDS:
723
811
  self.operand = name not in VALUE_KEYWORDS
@@ -937,10 +1025,13 @@ class Transpiler:
937
1025
  if all(p[0] for p in parts):
938
1026
  local = "".join(p[1] for p in parts)
939
1027
  return "%s.URIRef(%r)" % (RUNTIME_ALIAS, ns + local)
1028
+ # `ex:{expr}` EST un nom préfixé, pas une IRI formatée : même
1029
+ # concaténation, mais `pname` diffère l'évaluation quand la partie
1030
+ # locale porte une variable (fiche 017).
940
1031
  args = [repr(ns)]
941
1032
  for is_static, val in parts:
942
1033
  args.append(repr(val) if is_static else "(%s)" % val)
943
- return "%s.firi(%s)" % (RUNTIME_ALIAS, ", ".join(args))
1034
+ return "%s.pname(%s)" % (RUNTIME_ALIAS, ", ".join(args))
944
1035
 
945
1036
  # ------------------------------------------------------------------
946
1037
  # import de préfixes (fiche 013)
@@ -1624,6 +1715,24 @@ class Transpiler:
1624
1715
  # ------------------------------------------------------------------
1625
1716
 
1626
1717
  def _g_parse_triples(self):
1718
+ """Collects the island's terms as `parts` while parsing it.
1719
+
1720
+ Saved and restored around the call, because an interpolation can
1721
+ hold another island: `g{ ex:p f{ g{ ... } } }` runs a whole inner
1722
+ island — `_end_island` included — in the middle of the outer one.
1723
+ The finished list waits in `_pending_parts` for the `_end_island`
1724
+ that closes THIS island; an inner island finds it None and attaches
1725
+ nothing, which is what keeps the two from stealing each other's
1726
+ terms.
1727
+ """
1728
+ saved = self._parts
1729
+ self._parts = []
1730
+ try:
1731
+ return self._g_parse_triples_inner()
1732
+ finally:
1733
+ self._pending_parts, self._parts = self._parts, saved
1734
+
1735
+ def _g_parse_triples_inner(self):
1627
1736
  """Corps d'îlot de graphe : self.i juste après le '{' ouvrant.
1628
1737
  Consomme jusqu'au '}' fermant inclus ; retourne (triples, gctx)."""
1629
1738
  gctx = _GraphCtx()
@@ -1704,6 +1813,22 @@ class Transpiler:
1704
1813
  return
1705
1814
 
1706
1815
  def _g_verb(self, triples, gctx):
1816
+ """Records the predicate as a `part`, then parses it — the sibling
1817
+ of the wrapper on `_g_node`, for the position `_g_node` never sees."""
1818
+ if self._parts is None or self._sub:
1819
+ return self._g_verb_inner(triples, gctx)
1820
+ sl, sc, i0 = self.src_line, self.src_col, self.i
1821
+ gen = self._g_verb_inner(triples, gctx)
1822
+ # `a` is Turtle's rdf:type shorthand, not a prefixed name: no kind
1823
+ # of its own, so the island's hover answers for it
1824
+ kind = (None if self.text[i0:self.i].strip() == "a"
1825
+ else _term_kind(self.text[i0:self.i]))
1826
+ if kind is not None:
1827
+ self._parts.append(
1828
+ (kind, (sl, sc, self.src_line, self.src_col), gen))
1829
+ return gen
1830
+
1831
+ def _g_verb_inner(self, triples, gctx):
1707
1832
  t = self.text
1708
1833
  c = self._peek()
1709
1834
  if c == "a" and not (_name_char(self._peek(1)) or self._peek(1) == ":"):
@@ -1755,6 +1880,23 @@ class Transpiler:
1755
1880
  return "%s.Variable(%r)" % (RUNTIME_ALIAS, name)
1756
1881
 
1757
1882
  def _g_node(self, triples, gctx):
1883
+ """Records the term as a `part` of the island, then parses it.
1884
+
1885
+ `_g_parse_triples` opens the list; everything a composite island
1886
+ contains comes through here, so this one wrapper gives `g{ }`,
1887
+ `m{ }`, `+{ }` and `-{ }` a hover per term (record vscode/108)
1888
+ without touching the flat language map."""
1889
+ if self._parts is None or self._sub:
1890
+ return self._g_node_inner(triples, gctx)
1891
+ sl, sc, i0 = self.src_line, self.src_col, self.i
1892
+ gen, composite = self._g_node_inner(triples, gctx)
1893
+ kind = _term_kind(self.text[i0:self.i])
1894
+ if kind is not None:
1895
+ self._parts.append(
1896
+ (kind, (sl, sc, self.src_line, self.src_col), gen))
1897
+ return gen, composite
1898
+
1899
+ def _g_node_inner(self, triples, gctx):
1758
1900
  """Parse un nœud de graphe. Retourne (expr, is_composite) ;
1759
1901
  is_composite vrai pour [..], (..) qui peuvent être sujets sans props."""
1760
1902
  t = self.text
@@ -2485,7 +2627,8 @@ class _Term(str):
2485
2627
  def _maybe_impure(expr, gctx):
2486
2628
  """f-IRI et nom prefixe : impurs seulement s'ils portent une interpolation
2487
2629
  (sinon le transpileur a deja produit une URIRef constante)."""
2488
- if expr.startswith(RUNTIME_ALIAS + ".firi("):
2630
+ if expr.startswith(RUNTIME_ALIAS + ".firi(") \
2631
+ or expr.startswith(RUNTIME_ALIAS + ".pname("):
2489
2632
  return _impure(expr, gctx)
2490
2633
  return expr
2491
2634
 
@@ -12,14 +12,23 @@ import json
12
12
 
13
13
 
14
14
  class Segment:
15
- """One source/generated correspondence segment (copy, island:*, synthetic)."""
15
+ """One source/generated correspondence segment (copy, island:*, synthetic).
16
16
 
17
- __slots__ = ("kind", "src", "gen")
17
+ A composite island (`g{ }`, `m{ }`, `+{ }`, `-{ }`) also carries `parts`:
18
+ the terms written inside it, as `(kind, src, gen_text)` (record
19
+ vscode/108). They are deliberately NOT segments of their own — the map is
20
+ a flat, ordered list that breakpoint snapping, the source map and request
21
+ forwarding all walk, and nesting spans inside it would change what those
22
+ three answer. Parts are read by the hover and by nothing else.
23
+ """
18
24
 
19
- def __init__(self, kind, src, gen):
25
+ __slots__ = ("kind", "src", "gen", "parts")
26
+
27
+ def __init__(self, kind, src, gen, parts=None):
20
28
  self.kind = kind
21
29
  self.src = src # (line0, col0, line1, col1), or None for synthetic
22
30
  self.gen = gen # (line0, col0, line1, col1)
31
+ self.parts = parts or []
23
32
 
24
33
  def __repr__(self):
25
34
  return "Segment(%r, src=%r, gen=%r)" % (self.kind, self.src, self.gen)
@@ -29,6 +38,9 @@ class Segment:
29
38
  d = {"kind": self.kind, "gen": list(self.gen)}
30
39
  if self.src is not None:
31
40
  d["src"] = list(self.src)
41
+ if self.parts:
42
+ d["parts"] = [{"kind": k, "src": list(s), "gen": g}
43
+ for k, s, g in self.parts]
32
44
  return d
33
45
 
34
46
 
@@ -60,12 +72,12 @@ class LanguageMap:
60
72
  self.generated_name = generated_name
61
73
  self.segments = [] # ordered by increasing gen AND src positions
62
74
 
63
- def add(self, kind, src, gen):
75
+ def add(self, kind, src, gen, parts=None):
64
76
  """Add a segment (segments empty on both sides are ignored)."""
65
77
  # ignore segments that are empty on both sides
66
78
  if src is not None and src[:2] == src[2:] and gen[:2] == gen[2:]:
67
79
  return
68
- self.segments.append(Segment(kind, src, gen))
80
+ self.segments.append(Segment(kind, src, gen, parts))
69
81
 
70
82
  # -- traduction ---------------------------------------------------------
71
83
 
@@ -123,7 +135,10 @@ class LanguageMap:
123
135
  m = cls(d.get("source", "<ldpy>"), d.get("generated"))
124
136
  for sd in d.get("segments", []):
125
137
  src = tuple(sd["src"]) if "src" in sd else None
126
- m.segments.append(Segment(sd["kind"], src, tuple(sd["gen"])))
138
+ parts = [(pd["kind"], tuple(pd["src"]), pd["gen"])
139
+ for pd in sd.get("parts", ())]
140
+ m.segments.append(
141
+ Segment(sd["kind"], src, tuple(sd["gen"]), parts))
127
142
  return m
128
143
 
129
144
  @classmethod
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: linked-data-python
3
- Version: 0.2.0
3
+ Version: 0.5.1
4
4
  Summary: Python extended with Semantic Web primitives: IRIs, RDF literals and Turtle-notation graphs as expressions, transpiled by island parsing.
5
5
  Author-email: Maxime Lefrançois <maxime.lefrancois@emse.fr>
6
6
  License: MIT
@@ -96,31 +96,31 @@ From Python: `import ldpy; ldpy.install()` then `import yourmodule` finds
96
96
 
97
97
  ## Documentation
98
98
 
99
- Read it at [`docs/`](docs/README.md) — start with the
100
- [home page](docs/README.md) for an overview, then:
99
+ Read it at **<https://linked-data-python.readthedocs.io/>** — start with the
100
+ home page for an overview, then:
101
101
 
102
- - **Tutorials** — [first steps](docs/tutorials/getting-started.md), then
103
- [build a knowledge graph](docs/tutorials/build-a-knowledge-graph.md) from
102
+ - **Tutorials** — [first steps](https://linked-data-python.readthedocs.io/en/latest/tutorials/getting-started/), then
103
+ [build a knowledge graph](https://linked-data-python.readthedocs.io/en/latest/tutorials/build-a-knowledge-graph/) from
104
104
  tabular data.
105
- - **How-to guides** — [run & import](docs/how-to/run-and-import.md),
106
- [build graphs from tables](docs/how-to/build-graphs-from-tables.md),
107
- [read and query](docs/how-to/query-a-graph.md),
108
- [migrate from rdflib](docs/how-to/migrate-from-rdflib.md),
109
- [VS Code](docs/how-to/use-vscode.md), [debugging](docs/how-to/debug.md),
110
- [language server](docs/how-to/language-server.md),
111
- [highlighting](docs/how-to/highlight-ldpy.md).
112
- - **Reference** — [the language](docs/reference/language/index.md), one page
113
- per island family; [SPARQL expressions](docs/reference/sparql-expressions.md);
114
- [CLI](docs/reference/cli.md); [Python API](docs/reference/api.md);
115
- [language map formats](docs/reference/language-map.md).
116
- - **Explanation** — [why](docs/explanation/why.md),
117
- [what real RDF code does](docs/explanation/what-real-code-does.md) (the
105
+ - **How-to guides** — [run & import](https://linked-data-python.readthedocs.io/en/latest/how-to/run-and-import/),
106
+ [build graphs from tables](https://linked-data-python.readthedocs.io/en/latest/how-to/build-graphs-from-tables/),
107
+ [read and query](https://linked-data-python.readthedocs.io/en/latest/how-to/query-a-graph/),
108
+ [migrate from rdflib](https://linked-data-python.readthedocs.io/en/latest/how-to/migrate-from-rdflib/),
109
+ [VS Code](https://linked-data-python.readthedocs.io/en/latest/how-to/use-vscode/), [debugging](https://linked-data-python.readthedocs.io/en/latest/how-to/debug/),
110
+ [language server](https://linked-data-python.readthedocs.io/en/latest/how-to/language-server/),
111
+ [highlighting](https://linked-data-python.readthedocs.io/en/latest/how-to/highlight-ldpy/).
112
+ - **Reference** — [the language](https://linked-data-python.readthedocs.io/en/latest/reference/language/), one page
113
+ per island family; [SPARQL expressions](https://linked-data-python.readthedocs.io/en/latest/reference/sparql-expressions/);
114
+ [CLI](https://linked-data-python.readthedocs.io/en/latest/reference/cli/); [Python API](https://linked-data-python.readthedocs.io/en/latest/reference/api/);
115
+ [language map formats](https://linked-data-python.readthedocs.io/en/latest/reference/language-map/).
116
+ - **Explanation** — [why](https://linked-data-python.readthedocs.io/en/latest/explanation/why/),
117
+ [what real RDF code does](https://linked-data-python.readthedocs.io/en/latest/explanation/what-real-code-does/) (the
118
118
  corpus study that drove the language's second wave),
119
- [designing the syntax](docs/explanation/designing-the-syntax.md),
120
- [island parsing](docs/explanation/island-parsing.md),
121
- [emission & semantics](docs/explanation/emission-and-semantics.md),
122
- [tooling](docs/explanation/tooling.md),
123
- [how this is tested](docs/explanation/how-it-is-tested.md).
119
+ [designing the syntax](https://linked-data-python.readthedocs.io/en/latest/explanation/designing-the-syntax/),
120
+ [island parsing](https://linked-data-python.readthedocs.io/en/latest/explanation/island-parsing/),
121
+ [emission & semantics](https://linked-data-python.readthedocs.io/en/latest/explanation/emission-and-semantics/),
122
+ [tooling](https://linked-data-python.readthedocs.io/en/latest/explanation/tooling/),
123
+ [how this is tested](https://linked-data-python.readthedocs.io/en/latest/explanation/how-it-is-tested/).
124
124
 
125
125
  Every `ldpy` and `python` block in the documentation is executed by the test
126
126
  suite, and its assertions are the test.
@@ -0,0 +1,28 @@
1
+ ldpy/__init__.py,sha256=IyuzHrp5OtuGO_NrrdmMUQW6bTYcyS5uob7CeoXg8mY,1943
2
+ ldpy/__main__.py,sha256=YKTEjmNQuh_0p-KiEus9HngFs_ZDmS56RXzHTspiWVI,3077
3
+ ldpy/build.py,sha256=495F9KyFMar-hTCuaQkF3FvzWQX4mE9H0U0V-JuRDmA,3394
4
+ ldpy/console.py,sha256=1FrroxoTTWG7fIBzaDQvOxPGj6NkyXJDJhhl5RXyE_0,4204
5
+ ldpy/debug.py,sha256=5o8oVYIbFk6rB9-ShTIfym_bhVG3imTw_l0taJZ9qH0,9462
6
+ ldpy/formatter.py,sha256=VaDPECxhivsMe8jnX_fOL7BUyLEu5r4xREdr4Hav-0c,12695
7
+ ldpy/importer.py,sha256=QXAhSujPN6rFMJSQuj0UsWyI4CajirKt1SOrykJ_fUw,3670
8
+ ldpy/pygments_lexer.py,sha256=mcQ0u9nZjLnSWpqzcxd6bZjk69LfKSNTsf9A3rqfhhk,25320
9
+ ldpy/runtime.py,sha256=2xugLVjgtbay9GAsC_QGVFBeaXSRbja7xEkl2ZbYDQ4,36040
10
+ ldpy/sparql.py,sha256=CHSB4v5lEuVQKRPuwcM8xI3jN2FVHkmPVb675QkAD9Y,14770
11
+ ldpy/lsp/__init__.py,sha256=rK5GYOITi9PN09-WVlJ2aPKodW8dWTpgI_X6dFbP1zA,542
12
+ ldpy/lsp/__main__.py,sha256=MIp4ktqaoWZCNIMfv14bSC9pqK86JBS5XVjd1Z3c24o,72
13
+ ldpy/lsp/backend.py,sha256=8DnSGaBk1dsIMOBo7D1WQxxF3MZjJbYd78jp4ucWORY,5375
14
+ ldpy/lsp/hover.py,sha256=zSJ0PpU3kTJDXcwI2zSDCbsKrT-eNTt2RlvmEPRoV-M,2407
15
+ ldpy/lsp/islanddoc.py,sha256=vxxRKEcywBH31c8XgykEoC8fZfvbHok1NH-rPc-0Sy0,9540
16
+ ldpy/lsp/rpc.py,sha256=-TF_9U8dum23Pi4O1FZLogwoZgrGrt14J5BFB1OYVZw,3554
17
+ ldpy/lsp/server.py,sha256=PAMMookGUn00vAW_-a5BGy3qkVe9PoUdfzAzpdlvoF8,16359
18
+ ldpy/lsp/translate.py,sha256=HU9fBP_xWyYLih6QwQfSoyfF-MH-oInFcPX3o8Vc2GI,6116
19
+ ldpy/transpiler/__init__.py,sha256=9mQx0e47ixz0iMqSbCBbtGiwJ7PLxTcsVSIp6KNTMG4,504
20
+ ldpy/transpiler/core.py,sha256=4_YXbLLdtZfRTLb3ySprsnS_LXqiGfxZXqZvnXWT5T8,113482
21
+ ldpy/transpiler/errors.py,sha256=zEEwsNvC2Tawq8jIYls7hrbcWSvQtd7eo89k_SKp0k0,1142
22
+ ldpy/transpiler/linemap.py,sha256=ecLlCUkD4-6QQtUjGfAL28ql7LPITTS4fjLcDIbAwpQ,11386
23
+ linked_data_python-0.5.1.dist-info/licenses/LICENSE.md,sha256=R6KjQ098eAFd2k5mKaSRJqMH1LhY4o7xkHrKT5vjRfs,1074
24
+ linked_data_python-0.5.1.dist-info/METADATA,sha256=zC_1FxXgKCcqGYOcqNzUQkVVJ40m8HMKH-GzbMPsfWU,7974
25
+ linked_data_python-0.5.1.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
26
+ linked_data_python-0.5.1.dist-info/entry_points.txt,sha256=n8Vs52oH81BrKOe1DNUvdFLSOlEufRMpK2waPFa6aYk,224
27
+ linked_data_python-0.5.1.dist-info/top_level.txt,sha256=dkG_F4jYkh6CS6gdpQeV_gKW3yaJBvl1ukUu0zo2KA8,5
28
+ linked_data_python-0.5.1.dist-info/RECORD,,
@@ -1,26 +0,0 @@
1
- ldpy/__init__.py,sha256=ieLv4_enuK-Z6O-In0kp1zol08BsQwj2Ky1-wqb8Blo,1302
2
- ldpy/__main__.py,sha256=YKTEjmNQuh_0p-KiEus9HngFs_ZDmS56RXzHTspiWVI,3077
3
- ldpy/build.py,sha256=495F9KyFMar-hTCuaQkF3FvzWQX4mE9H0U0V-JuRDmA,3394
4
- ldpy/console.py,sha256=1FrroxoTTWG7fIBzaDQvOxPGj6NkyXJDJhhl5RXyE_0,4204
5
- ldpy/debug.py,sha256=mG8Dku-0erfJoBB9yYxI4eEuUipIpMsirLp6DfGx34k,9138
6
- ldpy/formatter.py,sha256=VaDPECxhivsMe8jnX_fOL7BUyLEu5r4xREdr4Hav-0c,12695
7
- ldpy/importer.py,sha256=QXAhSujPN6rFMJSQuj0UsWyI4CajirKt1SOrykJ_fUw,3670
8
- ldpy/pygments_lexer.py,sha256=mcQ0u9nZjLnSWpqzcxd6bZjk69LfKSNTsf9A3rqfhhk,25320
9
- ldpy/runtime.py,sha256=k1k_DfblW4Dxacu4BkZpfuEPV1mMN83PkLtML9SAoms,33390
10
- ldpy/sparql.py,sha256=CHSB4v5lEuVQKRPuwcM8xI3jN2FVHkmPVb675QkAD9Y,14770
11
- ldpy/lsp/__init__.py,sha256=rK5GYOITi9PN09-WVlJ2aPKodW8dWTpgI_X6dFbP1zA,542
12
- ldpy/lsp/__main__.py,sha256=MIp4ktqaoWZCNIMfv14bSC9pqK86JBS5XVjd1Z3c24o,72
13
- ldpy/lsp/backend.py,sha256=c5Jx1fZ1ZhoYet9__RB1mUqFUtcCl63tO8vgnv8iMII,4644
14
- ldpy/lsp/rpc.py,sha256=-TF_9U8dum23Pi4O1FZLogwoZgrGrt14J5BFB1OYVZw,3554
15
- ldpy/lsp/server.py,sha256=Xpl1ApcNlauLjEiabITOTJ6i8aVGDdPB7SgPxK2ePJs,14329
16
- ldpy/lsp/translate.py,sha256=_xDnKrJdx02puRkcuzhGwCLdEByuvhTfSIc9dAFR6a8,4931
17
- ldpy/transpiler/__init__.py,sha256=9mQx0e47ixz0iMqSbCBbtGiwJ7PLxTcsVSIp6KNTMG4,504
18
- ldpy/transpiler/core.py,sha256=s0jzTHSsazF7Xm-32q_Km6dKCxyjWEucmyVqd0eTP1s,106350
19
- ldpy/transpiler/errors.py,sha256=zEEwsNvC2Tawq8jIYls7hrbcWSvQtd7eo89k_SKp0k0,1142
20
- ldpy/transpiler/linemap.py,sha256=6b7y0nNmgnAMr_bqomgty7V0RgWPXFAp_8BZE4VYqzc,10580
21
- linked_data_python-0.2.0.dist-info/licenses/LICENSE.md,sha256=R6KjQ098eAFd2k5mKaSRJqMH1LhY4o7xkHrKT5vjRfs,1074
22
- linked_data_python-0.2.0.dist-info/METADATA,sha256=iHtvFy2zY921K5YLqF2vk13DGasgPwV2bSQPMdRGcgg,6985
23
- linked_data_python-0.2.0.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
24
- linked_data_python-0.2.0.dist-info/entry_points.txt,sha256=n8Vs52oH81BrKOe1DNUvdFLSOlEufRMpK2waPFa6aYk,224
25
- linked_data_python-0.2.0.dist-info/top_level.txt,sha256=dkG_F4jYkh6CS6gdpQeV_gKW3yaJBvl1ukUu0zo2KA8,5
26
- linked_data_python-0.2.0.dist-info/RECORD,,