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/runtime.py ADDED
@@ -0,0 +1,931 @@
1
+ """Runtime Linked-Data Python v2.
2
+
3
+ The code generated by the transpiler imports this module under the reserved
4
+ alias `_ldpy_` and uses ONLY this facade — never rdflib directly. rdflib is
5
+ the default backend; the facade allows an alternative one (urdflib / a
6
+ MicroPython implementation) without changing the generated code.
7
+
8
+ Emission and runtime choices are explained in docs/explanation/.
9
+ """
10
+
11
+ import itertools
12
+
13
+ import rdflib
14
+ from rdflib import RDF, BNode, Literal, Variable, Namespace
15
+ from rdflib.term import Node
16
+
17
+ _URI_CACHE = {}
18
+
19
+
20
+ def URIRef(value, base=None):
21
+ """rdflib.URIRef with a cache (key = the string). The IRIs emitted by the
22
+ transpiler are CONSTANTS of the program: without a cache, every turn of a
23
+ loop over a g{...} rebuilt them (see OPTIMIZATION.md). The cache is
24
+ bounded by the text of the programs; a 1M-entry guard covers dynamic uses
25
+ through the API."""
26
+ if base is not None:
27
+ return rdflib.URIRef(value, base)
28
+ u = _URI_CACHE.get(value)
29
+ if u is None:
30
+ if len(_URI_CACHE) > 1_000_000:
31
+ return rdflib.URIRef(value)
32
+ u = _URI_CACHE.setdefault(value, rdflib.URIRef(value))
33
+ return u
34
+
35
+ try:
36
+ from urllib.parse import urljoin as _urljoin
37
+ except ImportError: # MicroPython
38
+ def _urljoin(base, rel):
39
+ return base + rel
40
+
41
+ import re
42
+ _SCHEME_RE = re.compile(r"^[A-Za-z][A-Za-z0-9+.\-]*:")
43
+
44
+ __all__ = [
45
+ "RDF", "URIRef", "BNode", "Literal", "Variable", "Namespace",
46
+ "node", "bn", "slot", "firi", "bnode", "dtype", "graph", "instantiateBGP",
47
+ "pname", "new_graph", "add_to", "remove_from", "match", "prepared",
48
+ "Bindings", "as_bindings_iter", "Coercion",
49
+ "sparql",
50
+ ]
51
+
52
+
53
+ def __getattr__(name):
54
+ """Load ldpy.sparql on demand (avoids a circular import: sparql imports
55
+ node() from here)."""
56
+ if name == "sparql":
57
+ from ldpy import sparql as _s
58
+ globals()["sparql"] = _s
59
+ return _s
60
+ raise AttributeError(name)
61
+
62
+
63
+ class bn:
64
+ """A blank-node placeholder inside a graph() call.
65
+
66
+ The index is deterministic (syntactic position in the source g{...});
67
+ graph() creates a fresh BNode per index AT EVERY evaluation. The
68
+ instances, immutable, are pooled (one per index)."""
69
+
70
+ __slots__ = ("index",)
71
+ _pool = {}
72
+
73
+ def __new__(cls, index):
74
+ inst = cls._pool.get(index)
75
+ if inst is None:
76
+ inst = super().__new__(cls)
77
+ object.__setattr__(inst, "index", index)
78
+ cls._pool[index] = inst
79
+ return inst
80
+
81
+ def __init__(self, index):
82
+ pass
83
+
84
+ def __repr__(self):
85
+ return "bn(%d)" % self.index
86
+
87
+
88
+ class slot:
89
+ """A term shared by several triples of the same g{...}.
90
+
91
+ A term coming from an interpolation (`ex:{expr}`, `f<...{expr}...>`,
92
+ `{expr}`) that is the subject of several triples must be evaluated ONLY
93
+ ONCE: the expression is emitted at its first occurrence, `slot(i)` recalls
94
+ ensuite. Voir docs/explanation/emission-and-semantics.md."""
95
+
96
+ __slots__ = ("index", "value", "bound")
97
+
98
+ def __init__(self, index, *value):
99
+ self.index = index
100
+ self.bound = bool(value)
101
+ self.value = value[0] if value else None
102
+
103
+ def __repr__(self):
104
+ return "slot(%d)" % self.index
105
+
106
+
107
+ _BNODE_SAFE = re.compile(r"[A-Za-z0-9_][A-Za-z0-9_\-]*\Z")
108
+
109
+
110
+ def bnode(value):
111
+ """A blank node with a DETERMINISTIC IDENTITY derived from the data: the
112
+ ``_:{expr}`` .
113
+
114
+ - un BNode passe tel quel ;
115
+ - a string that is a safe label (letters/digits/_/-) becomes the label
116
+ itself: ``_:{key}`` == BNode(key);
117
+ - everything else — tuples in particular — is canonically encoded then
118
+ (``_:{(fname, lname)}`` ne
119
+ collide with ``_:{fname + lname}``, and the label produced stays
120
+ serialisable in N-Triples).
121
+
122
+ Unlike ``_:label`` (fresh at every evaluation, scoped to the island),
123
+ ``_:{expr}`` denotes THE SAME node wherever the value is equal — this is
124
+ the deduplication and blank-node-join idiom of R2RML (cases RMLTC0012a/b).
125
+ R2RML (cas RMLTC0012a/b)."""
126
+ if isinstance(value, BNode):
127
+ return value
128
+ if isinstance(value, str) and _BNODE_SAFE.match(value):
129
+ return _bnode_cached(value)
130
+ if isinstance(value, tuple):
131
+ canon = "\x1f".join("%d:%s" % (len(str(p)), p) for p in value)
132
+ else:
133
+ canon = "%s:%r" % (type(value).__name__, value)
134
+ import hashlib
135
+ return _bnode_cached("b" + hashlib.md5(canon.encode("utf-8")).hexdigest())
136
+
137
+
138
+ def _bnode_cached(label, _cache={}):
139
+ """Reuse the existing BNode object for a label already seen (deduplication
140
+ workloads keep coming back to the same keys)."""
141
+ b = _cache.get(label)
142
+ if b is None:
143
+ if len(_cache) > 1_000_000:
144
+ return BNode(label)
145
+ b = _cache.setdefault(label, BNode(label))
146
+ return b
147
+
148
+
149
+ _PASSTHROUGH = frozenset((rdflib.URIRef, Literal, BNode, Variable, bn))
150
+
151
+
152
+ _COERCION_STACK = []
153
+
154
+
155
+ class Coercion:
156
+ """Python -> RDF conversion policy (record ldpy/020): a value one names,
157
+ passes around and reuses — not a global setting.
158
+
159
+ Dictionary keys: a tuple of field names, or a Python type (resolved after
160
+ the field, through the MRO). Conversions: a datatype (IRI), URIRef to
161
+ build an IRI, or any function returning a term.
162
+ Scope: `with policy:` pushes then pops (an inner with refines the outer
163
+ one); `policy.install()` pushes without popping — a library uses with, an
164
+ application may install()."""
165
+
166
+ def __init__(self, rules):
167
+ self._fields = {}
168
+ self._types = {}
169
+ for key, conv in rules.items():
170
+ if isinstance(key, tuple):
171
+ for f in key:
172
+ self._fields[f] = conv
173
+ elif isinstance(key, type):
174
+ self._types[key] = conv
175
+ else:
176
+ raise TypeError(
177
+ "Coercion key: a tuple of field names or a Python type, "
178
+ "got %r" % (key,))
179
+
180
+ def __enter__(self):
181
+ """Push the policy for the duration of the with."""
182
+ _COERCION_STACK.append(self)
183
+ return self
184
+
185
+ def __exit__(self, *exc):
186
+ """Pop, exceptions included."""
187
+ _COERCION_STACK.pop()
188
+ return False
189
+
190
+ def install(self):
191
+ """Push for good (a module-wide or application-wide policy)."""
192
+ _COERCION_STACK.append(self)
193
+ return self
194
+
195
+ def _rule(self, field, value_type):
196
+ conv = self._fields.get(field) if field is not None else None
197
+ if conv is None:
198
+ for klass in value_type.__mro__:
199
+ conv = self._types.get(klass)
200
+ if conv is not None:
201
+ break
202
+ return conv
203
+
204
+
205
+ def _apply_conv(conv, value):
206
+ """Apply a Coercion conversion: datatype, IRI, or function."""
207
+ if conv is rdflib.URIRef or conv is URIRef:
208
+ return rdflib.URIRef(str(value))
209
+ if conv is Literal:
210
+ return Literal(value)
211
+ if isinstance(conv, rdflib.URIRef): # un datatype (XSD.integer, ...)
212
+ return Literal(value, datatype=conv)
213
+ return node(conv(value))
214
+
215
+
216
+ def node(value, field=None):
217
+ """Coerce a Python value into an RDF term (fnode / interpolations,
218
+ bindings, instantiation — the single entry point of record ldpy/020).
219
+ An already built RDF term is never reconverted. With no policy pushed,
220
+ Python -> Literal, rdflib choosing the datatype.
221
+ Dispatch on the exact type first: rdflib's isinstance/ABC path dominated
222
+ the materialisation profile (see OPTIMIZATION.md)."""
223
+ if type(value) in _PASSTHROUGH:
224
+ return value
225
+ if isinstance(value, (Node, bn)):
226
+ return value
227
+ if _COERCION_STACK:
228
+ vt = type(value)
229
+ for pol in reversed(_COERCION_STACK):
230
+ conv = pol._rule(field, vt)
231
+ if conv is not None:
232
+ return _apply_conv(conv, value)
233
+ return Literal(value)
234
+
235
+
236
+ def dtype(value):
237
+ """Coerce a value into a datatype IRI ({expr} after '^^').
238
+
239
+ A datatype is ALWAYS an IRI: unlike node(), a string is therefore read as
240
+ an IRI and not as a literal. Also accepts an object exposing its IRI
241
+ through str() (URIRef, a DefinedNamespace term)."""
242
+ if type(value) is rdflib.URIRef:
243
+ return value
244
+ if isinstance(value, str):
245
+ return URIRef(value)
246
+ return URIRef(str(value))
247
+
248
+
249
+ def pname(ns, *parts):
250
+ """A dynamic prefixed name (record ldpy/013): concatenates the namespace
251
+ IRI (imported prefix, or computed IRI) and the local part."""
252
+ return rdflib.URIRef(
253
+ str(ns) + "".join(p if isinstance(p, str) else str(p) for p in parts))
254
+
255
+
256
+ def firi(*parts, base=None):
257
+ """A formatted IRI: concatenate str(part), then resolve against base if relative."""
258
+ iri = "".join(p if isinstance(p, str) else str(p) for p in parts)
259
+ if base and not _SCHEME_RE.match(iri):
260
+ iri = _urljoin(base, iri)
261
+ return URIRef(iri)
262
+
263
+
264
+ _graph_ids = itertools.count()
265
+ _NM_CACHE = {}
266
+
267
+
268
+ def _nm_for(namespaces):
269
+ """A COSMETIC NamespaceManager, shared for a given state of the
270
+ __namespaces__ dict: binding the prefixes costs ~100x creating the graph,
271
+ and a g{...} evaluated in a loop pays that price every turn (see
272
+ OPTIMIZATION.md). The manager is cached and attached to the graphs
273
+ produced — the same sharing pattern as instantiateBGP. The cache is
274
+ invalidated by a snapshot of the content (identity alone is not enough:
275
+ block scope obliges)."""
276
+ if not namespaces:
277
+ return None
278
+ key = id(namespaces)
279
+ snap = tuple(sorted((p, str(u)) for p, u in namespaces.items()))
280
+ hit = _NM_CACHE.get(key)
281
+ if hit is not None and hit[0] == snap:
282
+ return hit[1]
283
+ holder = rdflib.Graph()
284
+ nm = holder.namespace_manager
285
+ for prefix, ns in namespaces.items():
286
+ nm.bind(prefix, ns, replace=True)
287
+ _NM_CACHE[key] = (snap, nm)
288
+ return nm
289
+
290
+
291
+ class _EmittedGraph(rdflib.Graph):
292
+ """The graph emitted by g{...}, materialised LAZILY.
293
+
294
+ The triples wait in a list; the store is only populated on the first real
295
+ access. rdflib reads its storage through the private attribute
296
+ ``_Graph__store``: a PROPERTY of the same name (a data descriptor, hence
297
+ taking precedence over the instance attribute) is the single point of
298
+ passage — any internal rdflib access materialises first. But ``__iter__``
299
+ serves the list WITHOUT materialising: ``target += g{...}`` then transfers
300
+ the triples directly — ONE store insertion instead of two (a measured
301
+ bottleneck: half the per-row materialisation cost)."""
302
+
303
+ __slots__ = ("_real_store", "_pending")
304
+
305
+ def __init__(self, *args, **kw):
306
+ self._real_store = None
307
+ self._pending = None
308
+ super().__init__(*args, **kw)
309
+
310
+ @property
311
+ def _Graph__store(self):
312
+ pending = self._pending
313
+ if pending is not None:
314
+ # `is not None`, not the truthiness of the list: an EMPTY list
315
+ # must switch lazy mode off too, otherwise __len__/__iter__ would
316
+ # keep serving [] while the store itself fills up (g{ } then
317
+ # .add(), or `g += g{…}`).
318
+ self._pending = None
319
+ if pending:
320
+ self._real_store.addN(
321
+ (s, p, o, self) for s, p, o in dict.fromkeys(pending))
322
+ return self._real_store
323
+
324
+ @_Graph__store.setter
325
+ def _Graph__store(self, store):
326
+ self._real_store = store
327
+
328
+ def __iter__(self):
329
+ pending = self._pending
330
+ if pending is not None:
331
+ return iter(dict.fromkeys(pending))
332
+ return super().__iter__()
333
+
334
+ def __len__(self):
335
+ pending = self._pending
336
+ if pending is not None:
337
+ return len(dict.fromkeys(pending))
338
+ return super().__len__()
339
+
340
+ def __call__(self, bindings=None):
341
+ """g{ ... }(b): the template instantiated against the mapping b — the
342
+ call suffix of record ldpy/019. Bound variables and deferred
343
+ expressions become terms; a triple with a term still unbound is
344
+ dropped; blank nodes are fresh at every instantiation."""
345
+ pending = self._pending
346
+ if pending is None:
347
+ pending = list(super().__iter__())
348
+ g = _EmittedGraph(base=self.base,
349
+ identifier=rdflib.URIRef("urn:x-ldpy:g%d"
350
+ % next(_graph_ids)))
351
+ nm = self._Graph__namespace_manager
352
+ if nm is not None:
353
+ g.namespace_manager = nm
354
+ term0 = _materializer(bindings if bindings is not None else {},
355
+ keep_vars=False)
356
+ bm = {}
357
+
358
+ def term(x):
359
+ v = term0(x)
360
+ if type(v) is BNode:
361
+ b = bm.get(v)
362
+ if b is None:
363
+ b = bm[v] = BNode()
364
+ return b
365
+ return v
366
+
367
+ g._pending = [tr for tr in
368
+ ((term(s), term(p), term(o)) for s, p, o in pending)
369
+ if tr[0] is not None and tr[1] is not None
370
+ and tr[2] is not None]
371
+ return g
372
+
373
+ def _merged(self, left, right):
374
+ """Nouveau graphe paresseux contenant left puis right (listes)."""
375
+ g = _EmittedGraph(base=self.base,
376
+ identifier=rdflib.URIRef("urn:x-ldpy:g%d"
377
+ % next(_graph_ids)))
378
+ nm = self._Graph__namespace_manager
379
+ if nm is not None:
380
+ g.namespace_manager = nm
381
+ g._pending = left + right
382
+ return g
383
+
384
+ def __add__(self, other):
385
+ """Lazy g1 + g2: concatenates the pending lists, no store involved.
386
+
387
+ Compositional addition (templates returning sums of g{...}) becomes
388
+ O(total) instead of O(n²) store insertions; deduplication (union
389
+ semantics) is still guaranteed at flush time."""
390
+ pending = self._pending
391
+ if pending is not None and isinstance(other, rdflib.Graph):
392
+ if type(other) is _EmittedGraph and other._pending is not None:
393
+ return self._merged(pending, other._pending)
394
+ return self._merged(pending, list(other))
395
+ return super().__add__(other)
396
+
397
+ def __iadd__(self, other):
398
+ """g += other, lazy as long as nothing has been materialised: the
399
+ boucle d'accumulation `g = g{ } ; g += g{…}` reste O(total) en
400
+ store insertions instead of paying one per turn."""
401
+ pending = self._pending
402
+ if pending is not None and isinstance(other, rdflib.Graph):
403
+ if type(other) is _EmittedGraph and other._pending is not None:
404
+ self._pending = pending + other._pending
405
+ else:
406
+ self._pending = pending + list(other)
407
+ return self
408
+ return super().__iadd__(other)
409
+
410
+ def __radd__(self, other):
411
+ """Graph() + g{...} (and therefore sum(...)) with no materialisation."""
412
+ pending = self._pending
413
+ if pending is not None and isinstance(other, rdflib.Graph):
414
+ return self._merged(list(other), pending)
415
+ return NotImplemented
416
+
417
+
418
+ class Bindings(dict):
419
+ """The current bindings (record ldpy/017): a dict with str or Variable
420
+ keys (normalised to str), values coerced into RDF terms on assignment.
421
+ Everything Python can do to a dict works: b[?x] = v, update, del, **b,
422
+ iteration over the keys."""
423
+
424
+ def __init__(self, mapping=None):
425
+ dict.__init__(self)
426
+ if mapping is not None:
427
+ if not hasattr(mapping, "items"):
428
+ raise TypeError(
429
+ "Bindings expects a mapping, got %s"
430
+ % type(mapping).__name__)
431
+ for k, v in mapping.items():
432
+ self[k] = v
433
+
434
+ def __setitem__(self, key, value):
435
+ dict.__setitem__(self, str(key), node(value, field=str(key)))
436
+
437
+ def __getitem__(self, key):
438
+ return dict.__getitem__(self, str(key))
439
+
440
+ def __delitem__(self, key):
441
+ dict.__delitem__(self, str(key))
442
+
443
+ def __contains__(self, key):
444
+ return dict.__contains__(self, str(key))
445
+
446
+ def get(self, key, default=None):
447
+ """dict.get, str or Variable key."""
448
+ return dict.get(self, str(key), default)
449
+
450
+ def update(self, other=(), **kw):
451
+ """dict.update, going through __setitem__'s coercion."""
452
+ items = other.items() if hasattr(other, "items") else other
453
+ for k, v in items:
454
+ self[k] = v
455
+ for k, v in kw.items():
456
+ self[k] = v
457
+
458
+
459
+ def as_bindings_iter(iterable):
460
+ """'for @bindings in ...' (record ldpy/017): every element becomes the
461
+ current bindings of the body. An m{ ... } yields its solutions (anonymous
462
+ variables excluded); any other iterable must produce mappings."""
463
+ if isinstance(iterable, Match):
464
+ for sm in iterable.solutions():
465
+ b = Bindings()
466
+ for k, v in sm.items():
467
+ if not str(k).startswith("__bn"):
468
+ dict.__setitem__(b, str(k), v) # already terms
469
+ yield b
470
+ return
471
+ for item in iterable:
472
+ if isinstance(item, Bindings):
473
+ yield item
474
+ elif hasattr(item, "items"):
475
+ yield Bindings(item)
476
+ else:
477
+ raise TypeError(
478
+ "for @bindings in ...: the iterable must produce mappings, "
479
+ "got %s" % type(item).__name__)
480
+
481
+
482
+ _EXPR_CLASS = None
483
+
484
+
485
+ def _expr_class():
486
+ """The Expression class of ldpy.sparql, loaded on demand (None if the
487
+ module is unavailable)."""
488
+ global _EXPR_CLASS
489
+ if _EXPR_CLASS is None:
490
+ try:
491
+ from ldpy.sparql import Expression
492
+ _EXPR_CLASS = Expression
493
+ except ImportError:
494
+ _EXPR_CLASS = False
495
+ return _EXPR_CLASS
496
+
497
+
498
+ def _bind_get(bindings, var):
499
+ """A variable's value in a mapping with str or Variable keys;
500
+ None = unbound (the instantiateBGP convention)."""
501
+ v = bindings.get(var)
502
+ if v is None:
503
+ v = bindings.get(str(var))
504
+ return v
505
+
506
+
507
+ def _materializer(bindings=None, keep_vars=True):
508
+ """Build the term materialiser of the islands (records ldpy/014, 016, 017).
509
+
510
+ bn(i) -> a BNode fresh per evaluation (memoised per index); slot -> a
511
+ shared value; Variable -> the bound value if bound, else the Variable
512
+ itself (keep_vars, template regime) or None (triple to drop)."""
513
+ bnodes = {}
514
+ slots = {}
515
+
516
+ def _term(t):
517
+ tt = type(t)
518
+ if tt in _PASSTHROUGH:
519
+ if tt is bn:
520
+ b = bnodes.get(t.index)
521
+ if b is None:
522
+ b = bnodes[t.index] = BNode()
523
+ return b
524
+ if tt is Variable:
525
+ if bindings is not None:
526
+ v = _bind_get(bindings, t)
527
+ if v is not None:
528
+ return node(v, field=str(t))
529
+ return t if keep_vars else None
530
+ return t
531
+ if tt is slot:
532
+ if t.bound:
533
+ # recursion, not node(): the shared value may itself be a
534
+ # term to materialise — a deferred expression e{ … } / e<…>
535
+ # in the subject position of a predicate-object list comes
536
+ # through here (record ldpy/017).
537
+ slots[t.index] = _term(t.value)
538
+ return slots[t.index]
539
+ cls = _expr_class()
540
+ if cls and tt is cls:
541
+ # e{ ... } in term position (records ldpy/007, 017): deferred
542
+ # without bindings, evaluated against the current bindings
543
+ # otherwise — a SPARQL error leaves the term unbound, the triple
544
+ if bindings is None:
545
+ return t if keep_vars else None
546
+ try:
547
+ return node(t(bindings))
548
+ except Exception:
549
+ return None
550
+ return node(t)
551
+
552
+ return _term
553
+
554
+
555
+ def new_graph(namespaces, base, identifier=None):
556
+ """A graph created by '@graph as g' (record ldpy/014): a plain
557
+ rdflib.Graph, with the serialisation bindings of the prefixes in scope."""
558
+ g = rdflib.Graph(base=base, identifier=identifier)
559
+ nm = _nm_for(namespaces)
560
+ if nm is not None:
561
+ g.namespace_manager = nm
562
+ return g
563
+
564
+
565
+ def add_to(graph, *triples, bindings=None):
566
+ """'+{ ... }': instantiate and add to the current graph (record ldpy/014).
567
+ A triple with a term still unbound is dropped — one cannot write an
568
+ unknown term."""
569
+ term = _materializer(bindings, keep_vars=False)
570
+ for s, p, o in triples:
571
+ s2, p2, o2 = term(s), term(p), term(o)
572
+ if s2 is not None and p2 is not None and o2 is not None:
573
+ graph.add((s2, p2, o2))
574
+ return graph
575
+
576
+
577
+ def remove_from(graph, *patterns, bindings=None):
578
+ """'-{ ... }': removal from the current graph (record ldpy/014). An
579
+ unbound variable is a wildcard (rdflib's remove((s, p, None)) semantics);
580
+ with several patterns sharing a variable, DELETE WHERE by matching
581
+ (fiche 016)."""
582
+ has_vars = any(isinstance(x, (Variable, bn))
583
+ for tr in patterns for x in tr)
584
+ if len(patterns) > 1 and has_vars:
585
+ # DELETE WHERE: match the BGP (record ldpy/016) then remove the
586
+ # instantiated triples — collected first, the graph must not be
587
+ # modified while matching.
588
+ prepared_pats, _ = _match_prepare(patterns, bindings)
589
+ to_remove = set()
590
+ for sm in Match(graph, patterns, (), bindings).solutions():
591
+ for tr in prepared_pats:
592
+ inst = tuple(sm.get(x) if isinstance(x, Variable) else x
593
+ for x in tr)
594
+ if all(t is not None for t in inst):
595
+ to_remove.add(inst)
596
+ for tr in to_remove:
597
+ graph.remove(tr)
598
+ return graph
599
+ term = _materializer(bindings, keep_vars=True)
600
+ for s, p, o in patterns:
601
+ tr = tuple(None if isinstance(x, (Variable, bn)) else x
602
+ for x in (term(s), term(p), term(o)))
603
+ graph.remove(tr)
604
+ return graph
605
+
606
+
607
+ def graph(namespaces, base, *triples, bindings=None):
608
+ """Build an rdflib.Graph (a lazy subtype) from triples
609
+ aplatis.
610
+
611
+ namespaces: dict prefix -> Namespace (serialisation bindings, shared
612
+ through _nm_for); base: lexical base IRI (str or None);
613
+ triples : tuples (s, p, o) pouvant contenir des placeholders bn(i).
614
+ With no bindings, a g{ } with variables or e{ } stays a template; with
615
+ the current bindings in scope, it is instantiated (record ldpy/017) — a
616
+ triple with a term still unbound is dropped."""
617
+ g = _EmittedGraph(base=base,
618
+ identifier=rdflib.URIRef("urn:x-ldpy:g%d"
619
+ % next(_graph_ids)))
620
+ nm = _nm_for(namespaces)
621
+ if nm is not None:
622
+ g.namespace_manager = nm
623
+ _term = _materializer(bindings, keep_vars=(bindings is None))
624
+ if bindings is None:
625
+ g._pending = [(_term(s), _term(p), _term(o)) for s, p, o in triples]
626
+ else:
627
+ g._pending = [tr for tr in
628
+ ((_term(s), _term(p), _term(o))
629
+ for s, p, o in triples)
630
+ if tr[0] is not None and tr[1] is not None
631
+ and tr[2] is not None]
632
+ return g
633
+
634
+
635
+ # ---------------------------------------------------------------- m{ ... }
636
+
637
+ class Row(tuple):
638
+ """A solution of an m{ ... } of arity >= 2: an unpackable tuple, named
639
+ access (row.s) and access by variable (row[?v] or row['v'])."""
640
+
641
+ def __new__(cls, values, fields):
642
+ r = tuple.__new__(cls, values)
643
+ r._fields = fields
644
+ return r
645
+
646
+ def __getattr__(self, name):
647
+ try:
648
+ return self[self._fields.index(name)]
649
+ except ValueError:
650
+ raise AttributeError(name)
651
+
652
+ def __getitem__(self, key):
653
+ if isinstance(key, (str, Variable)):
654
+ return tuple.__getitem__(self, self._fields.index(str(key)))
655
+ return tuple.__getitem__(self, key)
656
+
657
+ def __repr__(self):
658
+ return "Row(%s)" % ", ".join(
659
+ "%s=%r" % (f, tuple.__getitem__(self, i))
660
+ for i, f in enumerate(self._fields))
661
+
662
+
663
+ def _match_prepare(patterns, bindings):
664
+ """Patterns ready for matching: bn -> an anonymous variable shared by
665
+ index, slot -> a value, variables bound by the bindings -> a term."""
666
+ init = {}
667
+ if bindings:
668
+ for k, v in bindings.items():
669
+ init[Variable(str(k))] = node(v)
670
+ out = []
671
+ slots = {}
672
+ for s, p, o in patterns:
673
+ tr = []
674
+ for t in (s, p, o):
675
+ tt = type(t)
676
+ if tt is bn:
677
+ t = Variable("__bn%d" % t.index)
678
+ elif tt is slot:
679
+ if t.bound:
680
+ slots[t.index] = node(t.value)
681
+ t = slots[t.index]
682
+ if isinstance(t, Variable) and t in init:
683
+ t = init[t]
684
+ tr.append(t)
685
+ out.append(tuple(tr))
686
+ return out, init
687
+
688
+
689
+ class Match:
690
+ """The value of an m{ ... } island (record ldpy/016): a nested-loop join
691
+ over graph.triples(), in the order written — no engine, no heuristic.
692
+ Lazy: iterating, testing or first() only matches as much as is needed.
693
+ """
694
+
695
+ __slots__ = ("graph", "patterns", "project", "bindings")
696
+
697
+ def __init__(self, graph, patterns, project, bindings=None):
698
+ self.graph = graph
699
+ self.patterns = patterns
700
+ self.project = project
701
+ self.bindings = bindings
702
+
703
+ def __call__(self, graph=None, bindings=None):
704
+ return Match(self.graph if graph is None else graph,
705
+ self.patterns, self.project,
706
+ self.bindings if bindings is None else bindings)
707
+
708
+ def solutions(self):
709
+ """Generator of solution mappings (Bindings variable -> term),
710
+ initial bindings included (projected, record ldpy/019)."""
711
+ if self.graph is None:
712
+ raise RuntimeError(
713
+ "m{ } with no graph: declare '@graph ...' in scope, or "
714
+ "apply the pattern to a graph — m{ ... }(g)")
715
+ patterns, init = _match_prepare(self.patterns, self.bindings)
716
+ graph = self.graph
717
+
718
+ def join(i, sm):
719
+ if i == len(patterns):
720
+ yield sm
721
+ return
722
+ pat = []
723
+ var_pos = []
724
+ for k, t in enumerate(patterns[i]):
725
+ if isinstance(t, Variable):
726
+ v = sm.get(t)
727
+ if v is None:
728
+ var_pos.append((k, t))
729
+ v = None
730
+ pat.append(v)
731
+ else:
732
+ pat.append(t)
733
+ for found in graph.triples(tuple(pat)):
734
+ sm2 = dict(sm)
735
+ ok = True
736
+ for k, var in var_pos:
737
+ val = found[k]
738
+ prev = sm2.get(var)
739
+ if prev is not None and prev != val:
740
+ ok = False
741
+ break
742
+ sm2[var] = val
743
+ if ok:
744
+ yield from join(i + 1, sm2)
745
+
746
+ yield from join(0, dict(init))
747
+
748
+ def __iter__(self):
749
+ proj = self.project
750
+ if len(proj) == 1:
751
+ v = Variable(proj[0])
752
+ for sm in self.solutions():
753
+ yield sm.get(v)
754
+ else:
755
+ vs = [Variable(x) for x in proj]
756
+ for sm in self.solutions():
757
+ yield Row((sm.get(x) for x in vs), list(proj))
758
+
759
+ def __bool__(self):
760
+ for _ in self.solutions():
761
+ return True
762
+ return False
763
+
764
+ def first(self):
765
+ """The first solution, or None if there is none (~ g.value)."""
766
+ for x in self:
767
+ return x
768
+ return None
769
+
770
+ def one(self):
771
+ """The single solution; raises if there are zero or several."""
772
+ it = iter(self)
773
+ try:
774
+ first = next(it)
775
+ except StopIteration:
776
+ raise ValueError("m{ }.one() : aucune solution")
777
+ for _ in it:
778
+ raise ValueError("m{ }.one() : plusieurs solutions")
779
+ return first
780
+
781
+ def count(self):
782
+ """Number of solutions — consumes the matching (len() fails)."""
783
+ n = 0
784
+ for _ in self.solutions():
785
+ n += 1
786
+ return n
787
+
788
+ def __repr__(self):
789
+ return "m{ %d motif(s), projection %r }" % (
790
+ len(self.patterns), tuple(self.project))
791
+
792
+
793
+ def match(graph, patterns, project, bindings=None):
794
+ """Build the value of an m{ ... } island (record ldpy/016)."""
795
+ return Match(graph, patterns, project, bindings)
796
+
797
+
798
+ # ---------------------------------------------------------------- s{ ... }
799
+
800
+ _SPARQL_CACHE = {} # (text, ns key) -> prepared query
801
+ _SPARQL_CACHE_MAX = 64 # bounded; hand-written (no lru_cache: record ldpy/008)
802
+
803
+
804
+ def _prepare_sparql(text, namespaces, update):
805
+ key = (text, update, tuple(sorted((k, str(v))
806
+ for k, v in namespaces.items())))
807
+ hit = _SPARQL_CACHE.get(key)
808
+ if hit is not None:
809
+ return hit
810
+ from rdflib.plugins.sparql import prepareQuery, prepareUpdate
811
+ prep = (prepareUpdate if update else prepareQuery)(
812
+ text, initNs=dict(namespaces))
813
+ if len(_SPARQL_CACHE) >= _SPARQL_CACHE_MAX:
814
+ _SPARQL_CACHE.pop(next(iter(_SPARQL_CACHE)))
815
+ _SPARQL_CACHE[key] = prep
816
+ return prep
817
+
818
+
819
+ class PreparedQuery:
820
+ """The value of an s{ ... } island (record ldpy/015): a prepared, lazy query.
821
+
822
+ Iterating it (or testing it) runs it on its graph; calling it binds it —
823
+ the call suffix of record ldpy/019: graph first, bindings second."""
824
+
825
+ __slots__ = ("text", "interps", "namespaces", "base",
826
+ "graph", "bindings", "update")
827
+
828
+ def __init__(self, text, interps, namespaces, base,
829
+ graph=None, bindings=None, update=False):
830
+ self.text = text
831
+ self.interps = interps
832
+ self.namespaces = namespaces
833
+ self.base = base
834
+ self.graph = graph
835
+ self.bindings = bindings
836
+ self.update = update
837
+
838
+ def __call__(self, graph=None, bindings=None):
839
+ return PreparedQuery(self.text, self.interps, self.namespaces,
840
+ self.base,
841
+ graph if graph is not None else self.graph,
842
+ bindings if bindings is not None else self.bindings,
843
+ self.update)
844
+
845
+ def _init_bindings(self):
846
+ init = {}
847
+ if self.bindings:
848
+ for k, v in self.bindings.items():
849
+ init[Variable(str(k))] = node(v)
850
+ for name, value in self.interps:
851
+ init[Variable(name)] = node(value)
852
+ return init
853
+
854
+ def _execute(self):
855
+ if self.graph is None:
856
+ raise RuntimeError(
857
+ "s{ } with no graph: declare '@graph ...' in scope, or "
858
+ "apply the query to a graph — s{ ... }(g)")
859
+ prep = _prepare_sparql(self.text, self.namespaces, self.update)
860
+ if self.update:
861
+ return self.graph.update(prep, initBindings=self._init_bindings())
862
+ return self.graph.query(prep, initBindings=self._init_bindings())
863
+
864
+ def execute(self):
865
+ """Run the query and return rdflib's result.
866
+
867
+ Iterating an island, or testing its truth value, is enough for SELECT,
868
+ ASK and CONSTRUCT. An UPDATE returns nothing to iterate: the form is
869
+ what triggers it — `s{ INSERT … }.execute()`."""
870
+ return self._execute()
871
+
872
+ def __iter__(self):
873
+ return iter(self._execute())
874
+
875
+ def __bool__(self):
876
+ res = self._execute()
877
+ if getattr(res, "type", None) == "ASK":
878
+ return bool(res.askAnswer)
879
+ return res is not None and bool(len(res))
880
+
881
+ def __repr__(self):
882
+ return "s{ %s }" % self.text
883
+
884
+
885
+ def prepared(text, interps, namespaces, base, graph=None, bindings=None,
886
+ update=False):
887
+ """Build the value of an s{ ... } island (record ldpy/015)."""
888
+ return PreparedQuery(text, interps, namespaces, base,
889
+ graph, bindings, update)
890
+
891
+
892
+ def instantiateBGP(input, solutionMappings, initialGraph=None):
893
+ """Instantiate a graph template (BGP) with solution mappings.
894
+
895
+ Carried over from v1 (ldpy/ldpy.py), functionally unchanged."""
896
+ if initialGraph is None:
897
+ initialGraph = rdflib.Graph(base=input.base)
898
+ initialGraph.namespace_manager = input.namespace_manager
899
+ if solutionMappings is None:
900
+ return initialGraph
901
+ if isinstance(solutionMappings, dict):
902
+ solutionMappings = [solutionMappings]
903
+ if not isinstance(solutionMappings, list):
904
+ raise AssertionError(
905
+ "solutionMappings must be a dict or a list of dicts")
906
+
907
+ def _instantiate(t, sm, bm):
908
+ if isinstance(t, Variable):
909
+ if t in sm:
910
+ value = sm[t]
911
+ if value is None:
912
+ return None
913
+ if not isinstance(value, Node):
914
+ value = node(value, field=str(t))
915
+ return value
916
+ return None
917
+ if isinstance(t, BNode):
918
+ if t not in bm:
919
+ bm[t] = BNode()
920
+ return bm[t]
921
+ return t
922
+
923
+ for sm in solutionMappings:
924
+ bm = {}
925
+ for s, p, o in input:
926
+ s2 = _instantiate(s, sm, bm)
927
+ p2 = _instantiate(p, sm, bm)
928
+ o2 = _instantiate(o, sm, bm)
929
+ if s2 is not None and p2 is not None and o2 is not None:
930
+ initialGraph.add((s2, p2, o2))
931
+ return initialGraph