linked-data-python 0.2.1__tar.gz → 0.5.2__tar.gz

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 (70) hide show
  1. {linked_data_python-0.2.1 → linked_data_python-0.5.2}/PKG-INFO +1 -1
  2. {linked_data_python-0.2.1 → linked_data_python-0.5.2}/ldpy/console.py +13 -0
  3. {linked_data_python-0.2.1 → linked_data_python-0.5.2}/ldpy/debug.py +24 -2
  4. {linked_data_python-0.2.1 → linked_data_python-0.5.2}/ldpy/lsp/backend.py +14 -0
  5. linked_data_python-0.5.2/ldpy/lsp/hover.py +62 -0
  6. linked_data_python-0.5.2/ldpy/lsp/islanddoc.py +215 -0
  7. {linked_data_python-0.2.1 → linked_data_python-0.5.2}/ldpy/lsp/server.py +79 -30
  8. {linked_data_python-0.2.1 → linked_data_python-0.5.2}/ldpy/lsp/translate.py +31 -0
  9. {linked_data_python-0.2.1 → linked_data_python-0.5.2}/ldpy/runtime.py +73 -5
  10. {linked_data_python-0.2.1 → linked_data_python-0.5.2}/ldpy/transpiler/core.py +160 -13
  11. {linked_data_python-0.2.1 → linked_data_python-0.5.2}/ldpy/transpiler/linemap.py +21 -6
  12. {linked_data_python-0.2.1 → linked_data_python-0.5.2}/linked_data_python.egg-info/PKG-INFO +1 -1
  13. {linked_data_python-0.2.1 → linked_data_python-0.5.2}/linked_data_python.egg-info/SOURCES.txt +4 -0
  14. {linked_data_python-0.2.1 → linked_data_python-0.5.2}/pyproject.toml +1 -1
  15. {linked_data_python-0.2.1 → linked_data_python-0.5.2}/tests/test_bindings.py +101 -0
  16. {linked_data_python-0.2.1 → linked_data_python-0.5.2}/tests/test_console.py +58 -0
  17. {linked_data_python-0.2.1 → linked_data_python-0.5.2}/tests/test_current_graph.py +53 -0
  18. {linked_data_python-0.2.1 → linked_data_python-0.5.2}/tests/test_debug.py +42 -0
  19. {linked_data_python-0.2.1 → linked_data_python-0.5.2}/tests/test_disambiguation.py +32 -0
  20. linked_data_python-0.5.2/tests/test_docs.py +103 -0
  21. linked_data_python-0.5.2/tests/test_hover_parts.py +137 -0
  22. linked_data_python-0.5.2/tests/test_islanddoc.py +90 -0
  23. {linked_data_python-0.2.1 → linked_data_python-0.5.2}/tests/test_lsp_server.py +99 -11
  24. linked_data_python-0.2.1/tests/test_docs.py +0 -46
  25. {linked_data_python-0.2.1 → linked_data_python-0.5.2}/LICENSE.md +0 -0
  26. {linked_data_python-0.2.1 → linked_data_python-0.5.2}/README.md +0 -0
  27. {linked_data_python-0.2.1 → linked_data_python-0.5.2}/ldpy/__init__.py +0 -0
  28. {linked_data_python-0.2.1 → linked_data_python-0.5.2}/ldpy/__main__.py +0 -0
  29. {linked_data_python-0.2.1 → linked_data_python-0.5.2}/ldpy/build.py +0 -0
  30. {linked_data_python-0.2.1 → linked_data_python-0.5.2}/ldpy/formatter.py +0 -0
  31. {linked_data_python-0.2.1 → linked_data_python-0.5.2}/ldpy/importer.py +0 -0
  32. {linked_data_python-0.2.1 → linked_data_python-0.5.2}/ldpy/lsp/__init__.py +0 -0
  33. {linked_data_python-0.2.1 → linked_data_python-0.5.2}/ldpy/lsp/__main__.py +0 -0
  34. {linked_data_python-0.2.1 → linked_data_python-0.5.2}/ldpy/lsp/rpc.py +0 -0
  35. {linked_data_python-0.2.1 → linked_data_python-0.5.2}/ldpy/pygments_lexer.py +0 -0
  36. {linked_data_python-0.2.1 → linked_data_python-0.5.2}/ldpy/sparql.py +0 -0
  37. {linked_data_python-0.2.1 → linked_data_python-0.5.2}/ldpy/transpiler/__init__.py +0 -0
  38. {linked_data_python-0.2.1 → linked_data_python-0.5.2}/ldpy/transpiler/errors.py +0 -0
  39. {linked_data_python-0.2.1 → linked_data_python-0.5.2}/linked_data_python.egg-info/dependency_links.txt +0 -0
  40. {linked_data_python-0.2.1 → linked_data_python-0.5.2}/linked_data_python.egg-info/entry_points.txt +0 -0
  41. {linked_data_python-0.2.1 → linked_data_python-0.5.2}/linked_data_python.egg-info/requires.txt +0 -0
  42. {linked_data_python-0.2.1 → linked_data_python-0.5.2}/linked_data_python.egg-info/top_level.txt +0 -0
  43. {linked_data_python-0.2.1 → linked_data_python-0.5.2}/setup.cfg +0 -0
  44. {linked_data_python-0.2.1 → linked_data_python-0.5.2}/tests/test_bench.py +0 -0
  45. {linked_data_python-0.2.1 → linked_data_python-0.5.2}/tests/test_build.py +0 -0
  46. {linked_data_python-0.2.1 → linked_data_python-0.5.2}/tests/test_call_suffix.py +0 -0
  47. {linked_data_python-0.2.1 → linked_data_python-0.5.2}/tests/test_charsets.py +0 -0
  48. {linked_data_python-0.2.1 → linked_data_python-0.5.2}/tests/test_coercion.py +0 -0
  49. {linked_data_python-0.2.1 → linked_data_python-0.5.2}/tests/test_debug_stepping.py +0 -0
  50. {linked_data_python-0.2.1 → linked_data_python-0.5.2}/tests/test_docstrings.py +0 -0
  51. {linked_data_python-0.2.1 → linked_data_python-0.5.2}/tests/test_examples.py +0 -0
  52. {linked_data_python-0.2.1 → linked_data_python-0.5.2}/tests/test_formatter.py +0 -0
  53. {linked_data_python-0.2.1 → linked_data_python-0.5.2}/tests/test_generator.py +0 -0
  54. {linked_data_python-0.2.1 → linked_data_python-0.5.2}/tests/test_graphs.py +0 -0
  55. {linked_data_python-0.2.1 → linked_data_python-0.5.2}/tests/test_identity.py +0 -0
  56. {linked_data_python-0.2.1 → linked_data_python-0.5.2}/tests/test_import_hook.py +0 -0
  57. {linked_data_python-0.2.1 → linked_data_python-0.5.2}/tests/test_islands_terms.py +0 -0
  58. {linked_data_python-0.2.1 → linked_data_python-0.5.2}/tests/test_linemap.py +0 -0
  59. {linked_data_python-0.2.1 → linked_data_python-0.5.2}/tests/test_lsp_rpc.py +0 -0
  60. {linked_data_python-0.2.1 → linked_data_python-0.5.2}/tests/test_lsp_translate.py +0 -0
  61. {linked_data_python-0.2.1 → linked_data_python-0.5.2}/tests/test_match_island.py +0 -0
  62. {linked_data_python-0.2.1 → linked_data_python-0.5.2}/tests/test_micropython_subset.py +0 -0
  63. {linked_data_python-0.2.1 → linked_data_python-0.5.2}/tests/test_prefix_import.py +0 -0
  64. {linked_data_python-0.2.1 → linked_data_python-0.5.2}/tests/test_prefix_scoping.py +0 -0
  65. {linked_data_python-0.2.1 → linked_data_python-0.5.2}/tests/test_pygments_lexer.py +0 -0
  66. {linked_data_python-0.2.1 → linked_data_python-0.5.2}/tests/test_scope_modifiers.py +0 -0
  67. {linked_data_python-0.2.1 → linked_data_python-0.5.2}/tests/test_semantics.py +0 -0
  68. {linked_data_python-0.2.1 → linked_data_python-0.5.2}/tests/test_sourcemap_v3.py +0 -0
  69. {linked_data_python-0.2.1 → linked_data_python-0.5.2}/tests/test_sparql_expr.py +0 -0
  70. {linked_data_python-0.2.1 → linked_data_python-0.5.2}/tests/test_sparql_island.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: linked-data-python
3
- Version: 0.2.1
3
+ Version: 0.5.2
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
@@ -70,6 +70,12 @@ class LdpyConsole(code.InteractiveConsole):
70
70
  self._prefixes = dict(prefixes or {})
71
71
  self._prefix_cols = {k: 0 for k in self._prefixes}
72
72
  self._base = base
73
+ # The current graph and the current bindings are declarations too:
74
+ # like @prefix, a top-level @graph must survive to the next entry
75
+ # (otherwise `+{ ... }` after `@graph as g` sees no graph at all).
76
+ self._graph_var = None
77
+ self._bindings_var = None
78
+ self._counter = 0
73
79
 
74
80
  def runsource(self, source, filename=None, symbol="single"):
75
81
  """Transpile then compile one entry; True = incomplete entry
@@ -79,6 +85,10 @@ class LdpyConsole(code.InteractiveConsole):
79
85
  t.prefixes = dict(self._prefixes)
80
86
  t._prefix_col = dict(self._prefix_cols)
81
87
  t.base = self._base
88
+ t._graph_var = self._graph_var
89
+ t._bindings_var = self._bindings_var
90
+ # fresh names must not collide with those of the previous entries
91
+ t._ns_counter = self._counter
82
92
  try:
83
93
  result = t.run()
84
94
  except LdpySyntaxError as e:
@@ -98,6 +108,9 @@ class LdpyConsole(code.InteractiveConsole):
98
108
  self._prefixes = dict(t.prefixes)
99
109
  self._prefix_cols = dict(t._prefix_col)
100
110
  self._base = t.base
111
+ self._graph_var = t._graph_var
112
+ self._bindings_var = t._bindings_var
113
+ self._counter = t._ns_counter
101
114
  for w in result.warnings:
102
115
  self.write(str(w) + "\n")
103
116
  self.runcode(code_obj)
@@ -92,11 +92,30 @@ def probe():
92
92
  #: re-exported: debug tooling looks for them here (pure functions, defined
93
93
  #: alongside the map in ldpy/transpiler/linemap.py)
94
94
  __all__ = ["stepping_rules", "probe", "package_dir", "load_map",
95
+ "shadow_rel",
95
96
  "translate_breakpoints", "translate_frames",
96
97
  "snap_breakpoint_line", "snap_breakpoint_lines",
97
98
  "run_direct", "main"]
98
99
 
99
100
 
101
+ def shadow_rel(source, root=None):
102
+ """Path of the shadow inside the output directory.
103
+
104
+ Without a root, the basename: the shadow sits beside the source, and two
105
+ files never meet. With one — a workspace whose whole build is gathered in
106
+ a single directory — the tree is mirrored, which is what keeps `a/m.ldpy`
107
+ and `b/m.ldpy` from writing the same `m.py`. A source outside the root
108
+ falls back to its basename rather than climbing out of the output
109
+ directory with `../`."""
110
+ name = os.path.basename(source)
111
+ if not root:
112
+ return name
113
+ rel = os.path.relpath(os.path.abspath(source), os.path.abspath(root))
114
+ if rel.startswith(os.pardir + os.sep) or rel == os.pardir:
115
+ return name
116
+ return rel
117
+
118
+
100
119
  def load_map(map_path):
101
120
  """Load a LanguageMap from a .ldpy.map file (JSON)."""
102
121
  with open(map_path, "r", encoding="utf-8") as f:
@@ -167,6 +186,10 @@ def main(argv=None):
167
186
  "straight in the .ldpy)")
168
187
  parser.add_argument("-o", "--out", default=DEFAULT_OUT,
169
188
  help="shadow directory (default: %(default)s)")
189
+ parser.add_argument("--root", metavar="DIR",
190
+ help="tree root the shadow mirrors: the .py is "
191
+ "written at OUT/<path of the source relative "
192
+ "to DIR> (default: OUT/<basename>)")
170
193
  parser.add_argument("--listen", metavar="HOTE:PORT",
171
194
  help="start debugpy listening (e.g. 127.0.0.1:5678)")
172
195
  parser.add_argument("--wait-for-client", action="store_true",
@@ -201,8 +224,7 @@ def main(argv=None):
201
224
 
202
225
  try:
203
226
  py_path, map_path, result = build_file(
204
- args.source, args.out,
205
- rel=os.path.basename(args.source))
227
+ args.source, args.out, rel=shadow_rel(args.source, args.root))
206
228
  except LdpySyntaxError as e:
207
229
  print(str(e), file=sys.stderr)
208
230
  return 1
@@ -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):
@@ -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)
@@ -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)
@@ -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
 
@@ -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)