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.
- ldpy/__init__.py +35 -11
- ldpy/__main__.py +75 -250
- ldpy/build.py +91 -0
- ldpy/console.py +116 -0
- ldpy/debug.py +235 -0
- ldpy/formatter.py +329 -0
- ldpy/importer.py +110 -0
- ldpy/lsp/__init__.py +11 -0
- ldpy/lsp/__main__.py +4 -0
- ldpy/lsp/backend.py +118 -0
- ldpy/lsp/rpc.py +104 -0
- ldpy/lsp/server.py +353 -0
- ldpy/lsp/translate.py +140 -0
- ldpy/pygments_lexer.py +613 -0
- ldpy/runtime.py +931 -0
- ldpy/sparql.py +551 -0
- ldpy/transpiler/__init__.py +14 -0
- ldpy/transpiler/core.py +2558 -0
- ldpy/transpiler/errors.py +38 -0
- ldpy/transpiler/linemap.py +292 -0
- linked_data_python-0.2.0.dist-info/METADATA +158 -0
- linked_data_python-0.2.0.dist-info/RECORD +26 -0
- {linked_data_python-0.0.4.dist-info → linked_data_python-0.2.0.dist-info}/WHEEL +1 -1
- linked_data_python-0.2.0.dist-info/entry_points.txt +9 -0
- {linked_data_python-0.0.4.dist-info → linked_data_python-0.2.0.dist-info/licenses}/LICENSE.md +0 -0
- {linked_data_python-0.0.4.dist-info → linked_data_python-0.2.0.dist-info}/top_level.txt +0 -0
- ldpy/grun/lib.py +0 -63
- ldpy/grun/util.py +0 -11
- ldpy/ldpy.py +0 -183
- ldpy/rewriter/IndentedStringWriter.py +0 -54
- ldpy/rewriter/LDPythonRewriter.py +0 -677
- ldpy/rewriter/MultiChannelTokenStream.py +0 -127
- ldpy/rewriter/Result.py +0 -49
- ldpy/rewriter/__init__.py +0 -7
- ldpy/rewriter/antlr/LDPythonLexer.py +0 -870
- ldpy/rewriter/antlr/LDPythonParser.py +0 -9336
- ldpy/rewriter/antlr/LDPythonVisitor.py +0 -573
- ldpy/sparql/builtin.py +0 -326
- linked_data_python-0.0.4.dist-info/METADATA +0 -139
- linked_data_python-0.0.4.dist-info/RECORD +0 -20
- 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
|