linked-data-python 0.5.2__py3-none-any.whl → 0.6.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- ldpy/__main__.py +4 -1
- ldpy/backend.py +90 -0
- ldpy/build.py +41 -6
- ldpy/runtime.py +177 -53
- ldpy/sparql.py +4 -4
- ldpy/transpiler/core.py +20 -3
- {linked_data_python-0.5.2.dist-info → linked_data_python-0.6.1.dist-info}/METADATA +1 -1
- {linked_data_python-0.5.2.dist-info → linked_data_python-0.6.1.dist-info}/RECORD +12 -11
- {linked_data_python-0.5.2.dist-info → linked_data_python-0.6.1.dist-info}/WHEEL +0 -0
- {linked_data_python-0.5.2.dist-info → linked_data_python-0.6.1.dist-info}/entry_points.txt +0 -0
- {linked_data_python-0.5.2.dist-info → linked_data_python-0.6.1.dist-info}/licenses/LICENSE.md +0 -0
- {linked_data_python-0.5.2.dist-info → linked_data_python-0.6.1.dist-info}/top_level.txt +0 -0
ldpy/__main__.py
CHANGED
|
@@ -30,6 +30,9 @@ def main(argv=None):
|
|
|
30
30
|
help="open the interactive console after the script.")
|
|
31
31
|
parser.add_argument("-m", "--map", action="store_true",
|
|
32
32
|
help="also write the language map (<source>.map).")
|
|
33
|
+
parser.add_argument("--target", choices=["micropython"], default=None,
|
|
34
|
+
help="transpile for a device: s{ } is refused here, "
|
|
35
|
+
"at build time, where the message can be read.")
|
|
33
36
|
parser.add_argument("source", nargs="?",
|
|
34
37
|
help=".ldpy (or .py) file to run.")
|
|
35
38
|
args = parser.parse_args(argv)
|
|
@@ -45,7 +48,7 @@ def main(argv=None):
|
|
|
45
48
|
with open(args.source, "r", encoding="utf-8") as f:
|
|
46
49
|
source = f.read()
|
|
47
50
|
try:
|
|
48
|
-
result = transpile(source, args.source)
|
|
51
|
+
result = transpile(source, args.source, target=args.target)
|
|
49
52
|
except LdpySyntaxError as e:
|
|
50
53
|
print(str(e), file=sys.stderr)
|
|
51
54
|
return 1
|
ldpy/backend.py
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
"""The RDF backend of the façade: rdflib on CPython, urdflib on MicroPython.
|
|
2
|
+
|
|
3
|
+
The generated code never touches rdflib: it goes through `ldpy.runtime`,
|
|
4
|
+
and `ldpy.runtime` takes its terms and graphs from HERE. So this module is
|
|
5
|
+
the one place that knows which library is underneath — and the contract a
|
|
6
|
+
backend must meet is exactly its public names (record urdflib/306 measures
|
|
7
|
+
them with `urdflib/tools/api_gap.py`).
|
|
8
|
+
|
|
9
|
+
Two backends:
|
|
10
|
+
|
|
11
|
+
* **rdflib**, whenever it imports — the default on CPython, and the only one
|
|
12
|
+
with a SPARQL engine (`s{ }`);
|
|
13
|
+
* **urdflib**, the MicroPython C module of the linked-data-python
|
|
14
|
+
organisation, when rdflib is absent. It has the terms and the graphs;
|
|
15
|
+
`Namespace`, `RDF` and `XSD` are pure Python and provided here.
|
|
16
|
+
|
|
17
|
+
The two are not exclusive options of the same kind: the backend is chosen
|
|
18
|
+
by what is importable, and SPARQL queries are a capability of one backend.
|
|
19
|
+
`e{ }` expressions (ldpy.sparql) are pure Python over the terms, and run on
|
|
20
|
+
both.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
try:
|
|
24
|
+
import rdflib as _lib
|
|
25
|
+
from rdflib import RDF, XSD, BNode, Literal, Namespace, Variable
|
|
26
|
+
from rdflib.term import Node
|
|
27
|
+
URIRef = _lib.URIRef
|
|
28
|
+
Graph = _lib.Graph
|
|
29
|
+
NAME = "rdflib"
|
|
30
|
+
HAS_SPARQL = True
|
|
31
|
+
HAS_NAMESPACE_MANAGER = True
|
|
32
|
+
except ImportError: # MicroPython
|
|
33
|
+
import urdflib as _lib
|
|
34
|
+
from urdflib import BNode, Literal, Node, URIRef, Variable, Graph
|
|
35
|
+
NAME = "urdflib"
|
|
36
|
+
HAS_SPARQL = False
|
|
37
|
+
HAS_NAMESPACE_MANAGER = False
|
|
38
|
+
|
|
39
|
+
class Namespace(str):
|
|
40
|
+
"""A namespace IRI whose attributes and items are terms — rdflib's
|
|
41
|
+
Namespace, reduced to what the emitted code uses."""
|
|
42
|
+
|
|
43
|
+
def __getattr__(self, name):
|
|
44
|
+
if name.startswith("__"):
|
|
45
|
+
raise AttributeError(name)
|
|
46
|
+
return URIRef(self + name)
|
|
47
|
+
|
|
48
|
+
def __getitem__(self, name):
|
|
49
|
+
return URIRef(self + name)
|
|
50
|
+
|
|
51
|
+
def term(self, name):
|
|
52
|
+
return URIRef(self + name)
|
|
53
|
+
|
|
54
|
+
RDF = Namespace("http://www.w3.org/1999/02/22-rdf-syntax-ns#")
|
|
55
|
+
XSD = Namespace("http://www.w3.org/2001/XMLSchema#")
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def new_graph(base=None, identifier=None):
|
|
59
|
+
"""A graph of the backend. rdflib's takes a base, urdflib's does not
|
|
60
|
+
(a base is a lexical matter, resolved by the transpiler already)."""
|
|
61
|
+
if NAME == "rdflib":
|
|
62
|
+
return Graph(base=base, identifier=identifier)
|
|
63
|
+
return Graph(identifier=identifier) if identifier is not None else Graph()
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def bind_namespaces(graph, namespaces):
|
|
67
|
+
"""Give `graph` the serialisation prefixes of `namespaces` (a dict
|
|
68
|
+
prefix -> namespace IRI). On rdflib the runtime shares a cached
|
|
69
|
+
NamespaceManager instead (record ldpy/021); this is the plain path."""
|
|
70
|
+
for prefix, ns in namespaces.items():
|
|
71
|
+
graph.bind(prefix, str(ns))
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def prepare_sparql(text, namespaces, update=False):
|
|
75
|
+
"""A prepared SPARQL query or update — rdflib's engine, or an error
|
|
76
|
+
that says which backend is missing it."""
|
|
77
|
+
if not HAS_SPARQL:
|
|
78
|
+
raise RuntimeError(
|
|
79
|
+
"s{ } needs a SPARQL engine, and the %s backend has none: "
|
|
80
|
+
"transpile with target='micropython' to be told at build time, "
|
|
81
|
+
"and use m{ } and e{ } on the device" % NAME)
|
|
82
|
+
from rdflib.plugins.sparql import prepareQuery, prepareUpdate
|
|
83
|
+
return (prepareUpdate if update else prepareQuery)(
|
|
84
|
+
text, initNs=dict(namespaces))
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
__all__ = ["NAME", "HAS_SPARQL", "HAS_NAMESPACE_MANAGER",
|
|
88
|
+
"URIRef", "BNode", "Literal", "Variable", "Node", "Graph",
|
|
89
|
+
"Namespace", "RDF", "XSD",
|
|
90
|
+
"new_graph", "bind_namespaces", "prepare_sparql"]
|
ldpy/build.py
CHANGED
|
@@ -16,7 +16,35 @@ from ldpy.transpiler import transpile, LdpySyntaxError
|
|
|
16
16
|
DEFAULT_OUT = ".ldpy-build"
|
|
17
17
|
|
|
18
18
|
|
|
19
|
-
|
|
19
|
+
#: What a device needs of ldpy at run time, and nothing else: the façade,
|
|
20
|
+
#: its backend, and the expressions of e{ }. The transpiler stays on the
|
|
21
|
+
#: host. `ldpy/__init__.py` is NOT copied — it imports the transpiler and
|
|
22
|
+
#: the import hook — and is replaced by the docstring below.
|
|
23
|
+
DEVICE_RUNTIME = ("runtime.py", "backend.py", "sparql.py")
|
|
24
|
+
DEVICE_INIT = ('"""ldpy runtime for a device: what `import ldpy.runtime as '
|
|
25
|
+
'_ldpy_` needs.\n\nWritten by `python -m ldpy.build --target '
|
|
26
|
+
'micropython`; the transpiler is not here.\n"""\n')
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def bundle_device_runtime(out_dir):
|
|
30
|
+
"""Copy the device runtime into <out_dir>/ldpy/, so that the emitted
|
|
31
|
+
files and what they import travel together. Returns the paths."""
|
|
32
|
+
here = os.path.dirname(os.path.abspath(__file__))
|
|
33
|
+
dst = os.path.join(out_dir, "ldpy")
|
|
34
|
+
os.makedirs(dst, exist_ok=True)
|
|
35
|
+
written = []
|
|
36
|
+
with open(os.path.join(dst, "__init__.py"), "w", encoding="utf-8") as f:
|
|
37
|
+
f.write(DEVICE_INIT)
|
|
38
|
+
written.append(os.path.join(dst, "__init__.py"))
|
|
39
|
+
for name in DEVICE_RUNTIME:
|
|
40
|
+
with open(os.path.join(here, name), "rb") as fi, \
|
|
41
|
+
open(os.path.join(dst, name), "wb") as fo:
|
|
42
|
+
fo.write(fi.read())
|
|
43
|
+
written.append(os.path.join(dst, name))
|
|
44
|
+
return written
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def build_file(src_path, out_dir, rel=None, target=None):
|
|
20
48
|
"""Transpile one file; returns (py_path, map_path, result)."""
|
|
21
49
|
with open(src_path, "r", encoding="utf-8") as f:
|
|
22
50
|
source = f.read()
|
|
@@ -24,7 +52,7 @@ def build_file(src_path, out_dir, rel=None):
|
|
|
24
52
|
stem = rel[:-5] if rel.endswith(".ldpy") else rel
|
|
25
53
|
py_path = os.path.join(out_dir, stem + ".py")
|
|
26
54
|
map_path = os.path.join(out_dir, stem + ".ldpy.map")
|
|
27
|
-
result = transpile(source, src_path)
|
|
55
|
+
result = transpile(source, src_path, target=target)
|
|
28
56
|
result.map.generated_name = py_path
|
|
29
57
|
os.makedirs(os.path.dirname(py_path) or ".", exist_ok=True)
|
|
30
58
|
with open(py_path, "w", encoding="utf-8") as f:
|
|
@@ -37,7 +65,7 @@ def build_file(src_path, out_dir, rel=None):
|
|
|
37
65
|
return py_path, map_path, result
|
|
38
66
|
|
|
39
67
|
|
|
40
|
-
def build_tree(root, out_dir):
|
|
68
|
+
def build_tree(root, out_dir, target=None):
|
|
41
69
|
"""Recursively transpile every .ldpy under root. Plain .py files are
|
|
42
70
|
copied as they are (a mixed package must stay importable)."""
|
|
43
71
|
built, errors = [], []
|
|
@@ -49,7 +77,7 @@ def build_tree(root, out_dir):
|
|
|
49
77
|
rel = os.path.relpath(src, root)
|
|
50
78
|
if name.endswith(".ldpy"):
|
|
51
79
|
try:
|
|
52
|
-
built.append(build_file(src, out_dir, rel))
|
|
80
|
+
built.append(build_file(src, out_dir, rel, target))
|
|
53
81
|
except LdpySyntaxError as e:
|
|
54
82
|
errors.append(e)
|
|
55
83
|
elif name.endswith(".py"):
|
|
@@ -68,16 +96,23 @@ def main(argv=None):
|
|
|
68
96
|
parser.add_argument("source", help=".ldpy file or directory")
|
|
69
97
|
parser.add_argument("-o", "--out", default=DEFAULT_OUT,
|
|
70
98
|
help="output directory (default: %(default)s)")
|
|
99
|
+
parser.add_argument("--target", choices=["micropython"], default=None,
|
|
100
|
+
help="build for a device: s{ } is refused, and the "
|
|
101
|
+
"ldpy runtime is copied next to the output")
|
|
71
102
|
args = parser.parse_args(argv)
|
|
72
103
|
|
|
104
|
+
if args.target == "micropython":
|
|
105
|
+
os.makedirs(args.out, exist_ok=True)
|
|
106
|
+
bundle_device_runtime(args.out)
|
|
73
107
|
if os.path.isdir(args.source):
|
|
74
|
-
built, errors = build_tree(args.source, args.out)
|
|
108
|
+
built, errors = build_tree(args.source, args.out, args.target)
|
|
75
109
|
for e in errors:
|
|
76
110
|
print(str(e), file=sys.stderr)
|
|
77
111
|
print("%d file(s) transpiled into %s" % (len(built), args.out))
|
|
78
112
|
return 1 if errors else 0
|
|
79
113
|
try:
|
|
80
|
-
py_path, _, result = build_file(args.source, args.out
|
|
114
|
+
py_path, _, result = build_file(args.source, args.out,
|
|
115
|
+
target=args.target)
|
|
81
116
|
except LdpySyntaxError as e:
|
|
82
117
|
print(str(e), file=sys.stderr)
|
|
83
118
|
return 1
|
ldpy/runtime.py
CHANGED
|
@@ -1,35 +1,33 @@
|
|
|
1
1
|
"""Runtime Linked-Data Python v2.
|
|
2
2
|
|
|
3
3
|
The code generated by the transpiler imports this module under the reserved
|
|
4
|
-
alias `_ldpy_` and uses ONLY this facade — never
|
|
5
|
-
|
|
6
|
-
MicroPython
|
|
4
|
+
alias `_ldpy_` and uses ONLY this facade — never an RDF library directly.
|
|
5
|
+
The library underneath is chosen by `ldpy.backend`: rdflib on the host,
|
|
6
|
+
urdflib on a MicroPython device. The generated code is the same on both.
|
|
7
7
|
|
|
8
8
|
Emission and runtime choices are explained in docs/explanation/.
|
|
9
9
|
"""
|
|
10
10
|
|
|
11
|
-
import
|
|
12
|
-
|
|
13
|
-
import
|
|
14
|
-
from rdflib import RDF, BNode, Literal, Variable, Namespace
|
|
15
|
-
from rdflib.term import Node
|
|
11
|
+
from ldpy import backend as _backend
|
|
12
|
+
from ldpy.backend import RDF, BNode, Literal, Variable, Namespace, Node
|
|
13
|
+
from ldpy.backend import URIRef as _URIRef, Graph as _Graph
|
|
16
14
|
|
|
17
15
|
_URI_CACHE = {}
|
|
18
16
|
|
|
19
17
|
|
|
20
18
|
def URIRef(value, base=None):
|
|
21
|
-
"""
|
|
22
|
-
transpiler are CONSTANTS of the program: without a cache, every
|
|
23
|
-
loop over a g{...} rebuilt them (see OPTIMIZATION.md). The
|
|
24
|
-
bounded by the text of the programs; a 1M-entry guard covers
|
|
25
|
-
through the API."""
|
|
19
|
+
"""The backend's URIRef with a cache (key = the string). The IRIs emitted
|
|
20
|
+
by the transpiler are CONSTANTS of the program: without a cache, every
|
|
21
|
+
turn of a loop over a g{...} rebuilt them (see OPTIMIZATION.md). The
|
|
22
|
+
cache is bounded by the text of the programs; a 1M-entry guard covers
|
|
23
|
+
dynamic uses through the API."""
|
|
26
24
|
if base is not None:
|
|
27
|
-
return
|
|
25
|
+
return _URIRef(value, base)
|
|
28
26
|
u = _URI_CACHE.get(value)
|
|
29
27
|
if u is None:
|
|
30
|
-
if len(_URI_CACHE) >
|
|
31
|
-
return
|
|
32
|
-
u = _URI_CACHE.setdefault(value,
|
|
28
|
+
if len(_URI_CACHE) > 1000000:
|
|
29
|
+
return _URIRef(value)
|
|
30
|
+
u = _URI_CACHE.setdefault(value, _URIRef(value))
|
|
33
31
|
return u
|
|
34
32
|
|
|
35
33
|
try:
|
|
@@ -152,7 +150,7 @@ def _bnode_cached(label, _cache={}):
|
|
|
152
150
|
return b
|
|
153
151
|
|
|
154
152
|
|
|
155
|
-
_PASSTHROUGH = frozenset((
|
|
153
|
+
_PASSTHROUGH = frozenset((_URIRef, Literal, BNode, Variable, bn))
|
|
156
154
|
|
|
157
155
|
|
|
158
156
|
_COERCION_STACK = []
|
|
@@ -210,11 +208,11 @@ class Coercion:
|
|
|
210
208
|
|
|
211
209
|
def _apply_conv(conv, value):
|
|
212
210
|
"""Apply a Coercion conversion: datatype, IRI, or function."""
|
|
213
|
-
if conv is
|
|
214
|
-
return
|
|
211
|
+
if conv is _URIRef or conv is URIRef:
|
|
212
|
+
return _URIRef(str(value))
|
|
215
213
|
if conv is Literal:
|
|
216
214
|
return Literal(value)
|
|
217
|
-
if isinstance(conv,
|
|
215
|
+
if isinstance(conv, _URIRef): # un datatype (XSD.integer, ...)
|
|
218
216
|
return Literal(value, datatype=conv)
|
|
219
217
|
return node(conv(value))
|
|
220
218
|
|
|
@@ -245,7 +243,7 @@ def dtype(value):
|
|
|
245
243
|
A datatype is ALWAYS an IRI: unlike node(), a string is therefore read as
|
|
246
244
|
an IRI and not as a literal. Also accepts an object exposing its IRI
|
|
247
245
|
through str() (URIRef, a DefinedNamespace term)."""
|
|
248
|
-
if type(value) is
|
|
246
|
+
if type(value) is _URIRef:
|
|
249
247
|
return value
|
|
250
248
|
if isinstance(value, str):
|
|
251
249
|
return URIRef(value)
|
|
@@ -269,7 +267,7 @@ def pname(ns, *parts):
|
|
|
269
267
|
tp = type(p)
|
|
270
268
|
if tp is Variable or (cls and tp is cls):
|
|
271
269
|
return _deferred_pname(ns, parts)
|
|
272
|
-
return
|
|
270
|
+
return _URIRef(
|
|
273
271
|
str(ns) + "".join(p if isinstance(p, str) else str(p) for p in parts))
|
|
274
272
|
|
|
275
273
|
|
|
@@ -292,7 +290,7 @@ def _deferred_pname(ns, parts):
|
|
|
292
290
|
out.append(str(p(sm)))
|
|
293
291
|
else:
|
|
294
292
|
out.append(p if isinstance(p, str) else str(p))
|
|
295
|
-
return
|
|
293
|
+
return _URIRef("".join(out))
|
|
296
294
|
|
|
297
295
|
src = str(ns) + "".join(
|
|
298
296
|
"?%s" % p if type(p) is Variable
|
|
@@ -308,7 +306,19 @@ def firi(*parts, base=None):
|
|
|
308
306
|
return URIRef(iri)
|
|
309
307
|
|
|
310
308
|
|
|
311
|
-
|
|
309
|
+
class _Counter:
|
|
310
|
+
"""itertools.count(), which MicroPython does not ship."""
|
|
311
|
+
__slots__ = ("n",)
|
|
312
|
+
|
|
313
|
+
def __init__(self):
|
|
314
|
+
self.n = 0
|
|
315
|
+
|
|
316
|
+
def __next__(self):
|
|
317
|
+
self.n += 1
|
|
318
|
+
return self.n - 1
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
_graph_ids = _Counter()
|
|
312
322
|
_NM_CACHE = {}
|
|
313
323
|
|
|
314
324
|
|
|
@@ -320,14 +330,14 @@ def _nm_for(namespaces):
|
|
|
320
330
|
produced — the same sharing pattern as instantiateBGP. The cache is
|
|
321
331
|
invalidated by a snapshot of the content (identity alone is not enough:
|
|
322
332
|
block scope obliges)."""
|
|
323
|
-
if not namespaces:
|
|
333
|
+
if not namespaces or not _backend.HAS_NAMESPACE_MANAGER:
|
|
324
334
|
return None
|
|
325
335
|
key = id(namespaces)
|
|
326
336
|
snap = tuple(sorted((p, str(u)) for p, u in namespaces.items()))
|
|
327
337
|
hit = _NM_CACHE.get(key)
|
|
328
338
|
if hit is not None and hit[0] == snap:
|
|
329
339
|
return hit[1]
|
|
330
|
-
holder =
|
|
340
|
+
holder = _Graph()
|
|
331
341
|
nm = holder.namespace_manager
|
|
332
342
|
for prefix, ns in namespaces.items():
|
|
333
343
|
nm.bind(prefix, ns, replace=True)
|
|
@@ -335,7 +345,14 @@ def _nm_for(namespaces):
|
|
|
335
345
|
return nm
|
|
336
346
|
|
|
337
347
|
|
|
338
|
-
|
|
348
|
+
def _new_emitted(base):
|
|
349
|
+
"""A fresh emitted graph with the next identifier — lazy over rdflib,
|
|
350
|
+
eager over urdflib (the class is chosen once, below)."""
|
|
351
|
+
return _EMITTED(base=base,
|
|
352
|
+
identifier=_URIRef("urn:x-ldpy:g%d" % next(_graph_ids)))
|
|
353
|
+
|
|
354
|
+
|
|
355
|
+
class _EmittedGraph(_Graph):
|
|
339
356
|
"""The graph emitted by g{...}, materialised LAZILY.
|
|
340
357
|
|
|
341
358
|
The triples wait in a list; the store is only populated on the first real
|
|
@@ -392,9 +409,7 @@ class _EmittedGraph(rdflib.Graph):
|
|
|
392
409
|
pending = self._pending
|
|
393
410
|
if pending is None:
|
|
394
411
|
pending = list(super().__iter__())
|
|
395
|
-
g =
|
|
396
|
-
identifier=rdflib.URIRef("urn:x-ldpy:g%d"
|
|
397
|
-
% next(_graph_ids)))
|
|
412
|
+
g = _new_emitted(self.base)
|
|
398
413
|
nm = self._Graph__namespace_manager
|
|
399
414
|
if nm is not None:
|
|
400
415
|
g.namespace_manager = nm
|
|
@@ -419,9 +434,7 @@ class _EmittedGraph(rdflib.Graph):
|
|
|
419
434
|
|
|
420
435
|
def _merged(self, left, right):
|
|
421
436
|
"""Nouveau graphe paresseux contenant left puis right (listes)."""
|
|
422
|
-
g =
|
|
423
|
-
identifier=rdflib.URIRef("urn:x-ldpy:g%d"
|
|
424
|
-
% next(_graph_ids)))
|
|
437
|
+
g = _new_emitted(self.base)
|
|
425
438
|
nm = self._Graph__namespace_manager
|
|
426
439
|
if nm is not None:
|
|
427
440
|
g.namespace_manager = nm
|
|
@@ -435,7 +448,7 @@ class _EmittedGraph(rdflib.Graph):
|
|
|
435
448
|
O(total) instead of O(n²) store insertions; deduplication (union
|
|
436
449
|
semantics) is still guaranteed at flush time."""
|
|
437
450
|
pending = self._pending
|
|
438
|
-
if pending is not None and isinstance(other,
|
|
451
|
+
if pending is not None and isinstance(other, _Graph):
|
|
439
452
|
if type(other) is _EmittedGraph and other._pending is not None:
|
|
440
453
|
return self._merged(pending, other._pending)
|
|
441
454
|
return self._merged(pending, list(other))
|
|
@@ -446,7 +459,7 @@ class _EmittedGraph(rdflib.Graph):
|
|
|
446
459
|
boucle d'accumulation `g = g{ } ; g += g{…}` reste O(total) en
|
|
447
460
|
store insertions instead of paying one per turn."""
|
|
448
461
|
pending = self._pending
|
|
449
|
-
if pending is not None and isinstance(other,
|
|
462
|
+
if pending is not None and isinstance(other, _Graph):
|
|
450
463
|
if type(other) is _EmittedGraph and other._pending is not None:
|
|
451
464
|
self._pending = pending + other._pending
|
|
452
465
|
else:
|
|
@@ -457,11 +470,72 @@ class _EmittedGraph(rdflib.Graph):
|
|
|
457
470
|
def __radd__(self, other):
|
|
458
471
|
"""Graph() + g{...} (and therefore sum(...)) with no materialisation."""
|
|
459
472
|
pending = self._pending
|
|
460
|
-
if pending is not None and isinstance(other,
|
|
473
|
+
if pending is not None and isinstance(other, _Graph):
|
|
461
474
|
return self._merged(list(other), pending)
|
|
462
475
|
return NotImplemented
|
|
463
476
|
|
|
464
477
|
|
|
478
|
+
class _EagerEmittedGraph(_Graph):
|
|
479
|
+
"""The graph emitted by g{...} over urdflib: EAGER. The laziness
|
|
480
|
+
above rides on rdflib's private store attribute; a C graph has none,
|
|
481
|
+
and adds in place at C speed, so the triples go straight in. The
|
|
482
|
+
algebra the emitted code relies on stays: g1 + g2, g += x, g{ }(b)."""
|
|
483
|
+
|
|
484
|
+
def __init__(self, base=None, identifier=None):
|
|
485
|
+
_Graph.__init__(self, identifier=identifier) if identifier is not None \
|
|
486
|
+
else _Graph.__init__(self)
|
|
487
|
+
self.base = base
|
|
488
|
+
self._pending = None
|
|
489
|
+
self._Graph__namespace_manager = None
|
|
490
|
+
self._nm_namespaces = None
|
|
491
|
+
|
|
492
|
+
@property
|
|
493
|
+
def namespace_manager(self):
|
|
494
|
+
return self._Graph__namespace_manager
|
|
495
|
+
|
|
496
|
+
@namespace_manager.setter
|
|
497
|
+
def namespace_manager(self, nm):
|
|
498
|
+
self._Graph__namespace_manager = nm
|
|
499
|
+
|
|
500
|
+
def _fill(self, triples):
|
|
501
|
+
for tr in triples:
|
|
502
|
+
self.add(tr)
|
|
503
|
+
return self
|
|
504
|
+
|
|
505
|
+
def __call__(self, bindings=None):
|
|
506
|
+
g = _new_emitted(self.base)
|
|
507
|
+
term0 = _materializer(bindings if bindings is not None else {},
|
|
508
|
+
keep_vars=False)
|
|
509
|
+
bm = {}
|
|
510
|
+
|
|
511
|
+
def term(x):
|
|
512
|
+
v = term0(x)
|
|
513
|
+
if type(v) is BNode:
|
|
514
|
+
b = bm.get(v)
|
|
515
|
+
if b is None:
|
|
516
|
+
b = bm[v] = BNode()
|
|
517
|
+
return b
|
|
518
|
+
return v
|
|
519
|
+
|
|
520
|
+
return g._fill(tr for tr in
|
|
521
|
+
((term(s), term(p), term(o)) for s, p, o in self)
|
|
522
|
+
if tr[0] is not None and tr[1] is not None
|
|
523
|
+
and tr[2] is not None)
|
|
524
|
+
|
|
525
|
+
def __add__(self, other):
|
|
526
|
+
return _new_emitted(self.base)._fill(self)._fill(other)
|
|
527
|
+
|
|
528
|
+
def __iadd__(self, other):
|
|
529
|
+
return self._fill(other)
|
|
530
|
+
|
|
531
|
+
def __radd__(self, other):
|
|
532
|
+
return _new_emitted(self.base)._fill(other)._fill(self)
|
|
533
|
+
|
|
534
|
+
|
|
535
|
+
#: The emitted-graph class of this backend: a C graph has no store hook.
|
|
536
|
+
_EMITTED = _EmittedGraph if _backend.NAME == "rdflib" else _EagerEmittedGraph
|
|
537
|
+
|
|
538
|
+
|
|
465
539
|
class Bindings(dict):
|
|
466
540
|
"""The current bindings (record ldpy/017): a dict with str or Variable
|
|
467
541
|
keys (normalised to str), values coerced into RDF terms on assignment.
|
|
@@ -625,10 +699,12 @@ def _materializer(bindings=None, keep_vars=True):
|
|
|
625
699
|
def new_graph(namespaces, base, identifier=None):
|
|
626
700
|
"""A graph created by '@graph as g' (record ldpy/014): a plain
|
|
627
701
|
rdflib.Graph, with the serialisation bindings of the prefixes in scope."""
|
|
628
|
-
g =
|
|
702
|
+
g = _backend.new_graph(base=base, identifier=identifier)
|
|
629
703
|
nm = _nm_for(namespaces)
|
|
630
704
|
if nm is not None:
|
|
631
705
|
g.namespace_manager = nm
|
|
706
|
+
elif namespaces and not _backend.HAS_NAMESPACE_MANAGER:
|
|
707
|
+
_backend.bind_namespaces(g, namespaces)
|
|
632
708
|
return g
|
|
633
709
|
|
|
634
710
|
|
|
@@ -682,21 +758,25 @@ def graph(namespaces, base, *triples, bindings=None):
|
|
|
682
758
|
With no bindings, a g{ } with variables or e{ } stays a template; with
|
|
683
759
|
the current bindings in scope, it is instantiated (record ldpy/017) — a
|
|
684
760
|
triple with a term still unbound is dropped."""
|
|
685
|
-
g =
|
|
686
|
-
identifier=rdflib.URIRef("urn:x-ldpy:g%d"
|
|
687
|
-
% next(_graph_ids)))
|
|
761
|
+
g = _new_emitted(base)
|
|
688
762
|
nm = _nm_for(namespaces)
|
|
689
763
|
if nm is not None:
|
|
690
764
|
g.namespace_manager = nm
|
|
765
|
+
elif namespaces and not _backend.HAS_NAMESPACE_MANAGER:
|
|
766
|
+
_backend.bind_namespaces(g, namespaces)
|
|
691
767
|
_term = _materializer(bindings, keep_vars=(bindings is None))
|
|
692
768
|
if bindings is None:
|
|
693
|
-
|
|
769
|
+
pending = [(_term(s), _term(p), _term(o)) for s, p, o in triples]
|
|
694
770
|
else:
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
771
|
+
pending = [tr for tr in
|
|
772
|
+
((_term(s), _term(p), _term(o))
|
|
773
|
+
for s, p, o in triples)
|
|
774
|
+
if tr[0] is not None and tr[1] is not None
|
|
775
|
+
and tr[2] is not None]
|
|
776
|
+
if _backend.NAME == "rdflib":
|
|
777
|
+
g._pending = pending
|
|
778
|
+
else:
|
|
779
|
+
g._fill(pending)
|
|
700
780
|
return g
|
|
701
781
|
|
|
702
782
|
|
|
@@ -728,6 +808,51 @@ class Row(tuple):
|
|
|
728
808
|
for i, f in enumerate(self._fields))
|
|
729
809
|
|
|
730
810
|
|
|
811
|
+
class _RowSeq:
|
|
812
|
+
"""The same Row for MicroPython, which cannot subclass tuple with
|
|
813
|
+
__new__: a small sequence that unpacks, compares to a tuple, and
|
|
814
|
+
answers to names and variables."""
|
|
815
|
+
|
|
816
|
+
__slots__ = ("_values", "_fields")
|
|
817
|
+
|
|
818
|
+
def __init__(self, values, fields):
|
|
819
|
+
self._values = tuple(values)
|
|
820
|
+
self._fields = fields
|
|
821
|
+
|
|
822
|
+
def __iter__(self):
|
|
823
|
+
return iter(self._values)
|
|
824
|
+
|
|
825
|
+
def __len__(self):
|
|
826
|
+
return len(self._values)
|
|
827
|
+
|
|
828
|
+
def __eq__(self, other):
|
|
829
|
+
return self._values == (other._values if isinstance(other, _RowSeq)
|
|
830
|
+
else other)
|
|
831
|
+
|
|
832
|
+
def __hash__(self):
|
|
833
|
+
return hash(self._values)
|
|
834
|
+
|
|
835
|
+
def __getattr__(self, name):
|
|
836
|
+
try:
|
|
837
|
+
return self._values[self._fields.index(name)]
|
|
838
|
+
except ValueError:
|
|
839
|
+
raise AttributeError(name)
|
|
840
|
+
|
|
841
|
+
def __getitem__(self, key):
|
|
842
|
+
if isinstance(key, (str, Variable)):
|
|
843
|
+
return self._values[self._fields.index(str(key))]
|
|
844
|
+
return self._values[key]
|
|
845
|
+
|
|
846
|
+
def __repr__(self):
|
|
847
|
+
return "Row(%s)" % ", ".join(
|
|
848
|
+
"%s=%r" % (f, v) for f, v in zip(self._fields, self._values))
|
|
849
|
+
|
|
850
|
+
|
|
851
|
+
#: The row class of this Python: tuple-based, unless tuple cannot be
|
|
852
|
+
#: subclassed with __new__ (MicroPython).
|
|
853
|
+
_ROW = Row if hasattr(tuple, "__new__") else _RowSeq
|
|
854
|
+
|
|
855
|
+
|
|
731
856
|
def _match_prepare(patterns, bindings):
|
|
732
857
|
"""Patterns ready for matching: bn -> an anonymous variable shared by
|
|
733
858
|
index, slot -> a value, variables bound by the bindings -> a term."""
|
|
@@ -822,7 +947,7 @@ class Match:
|
|
|
822
947
|
else:
|
|
823
948
|
vs = [Variable(x) for x in proj]
|
|
824
949
|
for sm in self.solutions():
|
|
825
|
-
yield
|
|
950
|
+
yield _ROW((sm.get(x) for x in vs), list(proj))
|
|
826
951
|
|
|
827
952
|
def __bool__(self):
|
|
828
953
|
for _ in self.solutions():
|
|
@@ -875,9 +1000,7 @@ def _prepare_sparql(text, namespaces, update):
|
|
|
875
1000
|
hit = _SPARQL_CACHE.get(key)
|
|
876
1001
|
if hit is not None:
|
|
877
1002
|
return hit
|
|
878
|
-
|
|
879
|
-
prep = (prepareUpdate if update else prepareQuery)(
|
|
880
|
-
text, initNs=dict(namespaces))
|
|
1003
|
+
prep = _backend.prepare_sparql(text, namespaces, update)
|
|
881
1004
|
if len(_SPARQL_CACHE) >= _SPARQL_CACHE_MAX:
|
|
882
1005
|
_SPARQL_CACHE.pop(next(iter(_SPARQL_CACHE)))
|
|
883
1006
|
_SPARQL_CACHE[key] = prep
|
|
@@ -962,8 +1085,9 @@ def instantiateBGP(input, solutionMappings, initialGraph=None):
|
|
|
962
1085
|
|
|
963
1086
|
Carried over from v1 (ldpy/ldpy.py), functionally unchanged."""
|
|
964
1087
|
if initialGraph is None:
|
|
965
|
-
initialGraph =
|
|
966
|
-
|
|
1088
|
+
initialGraph = _backend.new_graph(base=input.base)
|
|
1089
|
+
if _backend.HAS_NAMESPACE_MANAGER:
|
|
1090
|
+
initialGraph.namespace_manager = input.namespace_manager
|
|
967
1091
|
if solutionMappings is None:
|
|
968
1092
|
return initialGraph
|
|
969
1093
|
if isinstance(solutionMappings, dict):
|
ldpy/sparql.py
CHANGED
|
@@ -11,14 +11,14 @@ later, against a *solution mapping*:
|
|
|
11
11
|
The semantics follow SPARQL 1.1: numeric promotion (integer < decimal <
|
|
12
12
|
float < double, integer division -> decimal), errors PROPAGATED (unbound
|
|
13
13
|
variable, incomparable types) and absorbed by ``||``/``&&``/``IF``/
|
|
14
|
-
``COALESCE`` per SPARQL's three-valued truth table. No
|
|
15
|
-
|
|
14
|
+
``COALESCE`` per SPARQL's three-valued truth table. No parser of any RDF
|
|
15
|
+
library is used: the terms come from ``ldpy.backend`` — rdflib or urdflib —
|
|
16
|
+
so these expressions run on a device as well as on the host.
|
|
16
17
|
"""
|
|
17
18
|
|
|
18
19
|
import re as _re
|
|
19
20
|
|
|
20
|
-
from
|
|
21
|
-
from rdflib.term import Node
|
|
21
|
+
from ldpy.backend import BNode, Literal, URIRef, Variable, XSD, Node
|
|
22
22
|
|
|
23
23
|
from ldpy.runtime import node as _node
|
|
24
24
|
|
ldpy/transpiler/core.py
CHANGED
|
@@ -217,7 +217,18 @@ def _term_kind(text):
|
|
|
217
217
|
class Transpiler:
|
|
218
218
|
"""Une instance par fichier. Usage : Transpiler(src, filename).run()."""
|
|
219
219
|
|
|
220
|
-
|
|
220
|
+
#: The targets a programme can be transpiled for. `None` is the host;
|
|
221
|
+
#: "micropython" is a device whose backend has no SPARQL engine, so
|
|
222
|
+
#: s{ } is refused HERE, at build time on the host, where the message
|
|
223
|
+
#: can be read — not on the device, at the first query.
|
|
224
|
+
TARGETS = (None, "micropython")
|
|
225
|
+
|
|
226
|
+
def __init__(self, text, filename="<ldpy>", emit_prelude=True,
|
|
227
|
+
target=None):
|
|
228
|
+
if target not in self.TARGETS:
|
|
229
|
+
raise ValueError("unknown target %r (one of %s)"
|
|
230
|
+
% (target, ", ".join(repr(t) for t in self.TARGETS)))
|
|
231
|
+
self.target = target
|
|
221
232
|
self.text = text
|
|
222
233
|
self.emit_prelude = emit_prelude
|
|
223
234
|
self.n = len(text)
|
|
@@ -2090,6 +2101,11 @@ class Transpiler:
|
|
|
2090
2101
|
transpilation (l'oracle, fiche 015)."""
|
|
2091
2102
|
mark = self._begin_island()
|
|
2092
2103
|
isl_line, isl_col = self.src_line, self.src_col
|
|
2104
|
+
if self.target == "micropython":
|
|
2105
|
+
self._error("s{ } is not available on the micropython target: "
|
|
2106
|
+
"a SPARQL query needs rdflib's engine, which the "
|
|
2107
|
+
"device does not have — write the pattern with "
|
|
2108
|
+
"m{ } and the filter with e{ }", incomplete=False)
|
|
2093
2109
|
self._take(2) # s{
|
|
2094
2110
|
t = self.text
|
|
2095
2111
|
pieces = []
|
|
@@ -2697,9 +2713,10 @@ class _GraphCtx:
|
|
|
2697
2713
|
return "%s.bn(%d)" % (RUNTIME_ALIAS, self.labels[label])
|
|
2698
2714
|
|
|
2699
2715
|
|
|
2700
|
-
def transpile(source, filename="<ldpy>"):
|
|
2716
|
+
def transpile(source, filename="<ldpy>", target=None):
|
|
2701
2717
|
"""Transpile un source Linked-Data Python en Python.
|
|
2702
2718
|
|
|
2719
|
+
`target` : None (l'hôte) ou "micropython" — voir Transpiler.TARGETS.
|
|
2703
2720
|
Retourne un TranspileResult(code, map, prefixes, base, warnings).
|
|
2704
2721
|
"""
|
|
2705
|
-
return Transpiler(source, filename).run()
|
|
2722
|
+
return Transpiler(source, filename, target=target).run()
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: linked-data-python
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.6.1
|
|
4
4
|
Summary: Python extended with Semantic Web primitives: IRIs, RDF literals and Turtle-notation graphs as expressions, transpiled by island parsing.
|
|
5
5
|
Author-email: Maxime Lefrançois <maxime.lefrancois@emse.fr>
|
|
6
6
|
License: MIT
|
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
ldpy/__init__.py,sha256=IyuzHrp5OtuGO_NrrdmMUQW6bTYcyS5uob7CeoXg8mY,1943
|
|
2
|
-
ldpy/__main__.py,sha256=
|
|
3
|
-
ldpy/
|
|
2
|
+
ldpy/__main__.py,sha256=J1drZdQZ5jSAE5ri_NFB43jzkUuMgWHp-KL0Mjhbrhc,3328
|
|
3
|
+
ldpy/backend.py,sha256=SffPO4UQT_1p1JPhNiBtdlbNYNmpMySUPiLXvgR0Z2k,3585
|
|
4
|
+
ldpy/build.py,sha256=llHQ-7ndgg_yeMKukexrEsu0cAIFFU0JQSaL5_ywQss,5170
|
|
4
5
|
ldpy/console.py,sha256=vMSnFFoOIOcanw6_niNxqgrt-F4gzMOrD1GopkXru5s,4837
|
|
5
6
|
ldpy/debug.py,sha256=JUuzeKc8ya4SG30AxaUVeihTEbzySo1cS0sU0s78Mv0,10493
|
|
6
7
|
ldpy/formatter.py,sha256=VaDPECxhivsMe8jnX_fOL7BUyLEu5r4xREdr4Hav-0c,12695
|
|
7
8
|
ldpy/importer.py,sha256=QXAhSujPN6rFMJSQuj0UsWyI4CajirKt1SOrykJ_fUw,3670
|
|
8
9
|
ldpy/pygments_lexer.py,sha256=mcQ0u9nZjLnSWpqzcxd6bZjk69LfKSNTsf9A3rqfhhk,25320
|
|
9
|
-
ldpy/runtime.py,sha256=
|
|
10
|
-
ldpy/sparql.py,sha256=
|
|
10
|
+
ldpy/runtime.py,sha256=vkk9HQvUpi6pET0S4EqH1sA3797eA89NeTzHz9dLT_w,39632
|
|
11
|
+
ldpy/sparql.py,sha256=yPe-ZR1tpY--nM6q5LYXyXP7zlkE4gqhIRYAKwfpTBg,14851
|
|
11
12
|
ldpy/lsp/__init__.py,sha256=rK5GYOITi9PN09-WVlJ2aPKodW8dWTpgI_X6dFbP1zA,542
|
|
12
13
|
ldpy/lsp/__main__.py,sha256=MIp4ktqaoWZCNIMfv14bSC9pqK86JBS5XVjd1Z3c24o,72
|
|
13
14
|
ldpy/lsp/backend.py,sha256=8DnSGaBk1dsIMOBo7D1WQxxF3MZjJbYd78jp4ucWORY,5375
|
|
@@ -17,12 +18,12 @@ ldpy/lsp/rpc.py,sha256=-TF_9U8dum23Pi4O1FZLogwoZgrGrt14J5BFB1OYVZw,3554
|
|
|
17
18
|
ldpy/lsp/server.py,sha256=PAMMookGUn00vAW_-a5BGy3qkVe9PoUdfzAzpdlvoF8,16359
|
|
18
19
|
ldpy/lsp/translate.py,sha256=HU9fBP_xWyYLih6QwQfSoyfF-MH-oInFcPX3o8Vc2GI,6116
|
|
19
20
|
ldpy/transpiler/__init__.py,sha256=9mQx0e47ixz0iMqSbCBbtGiwJ7PLxTcsVSIp6KNTMG4,504
|
|
20
|
-
ldpy/transpiler/core.py,sha256=
|
|
21
|
+
ldpy/transpiler/core.py,sha256=LaZT8hE4jocIcTLyb2kc_mFtwu8QTSm0FhOMNetY_2k,114831
|
|
21
22
|
ldpy/transpiler/errors.py,sha256=zEEwsNvC2Tawq8jIYls7hrbcWSvQtd7eo89k_SKp0k0,1142
|
|
22
23
|
ldpy/transpiler/linemap.py,sha256=ecLlCUkD4-6QQtUjGfAL28ql7LPITTS4fjLcDIbAwpQ,11386
|
|
23
|
-
linked_data_python-0.
|
|
24
|
-
linked_data_python-0.
|
|
25
|
-
linked_data_python-0.
|
|
26
|
-
linked_data_python-0.
|
|
27
|
-
linked_data_python-0.
|
|
28
|
-
linked_data_python-0.
|
|
24
|
+
linked_data_python-0.6.1.dist-info/licenses/LICENSE.md,sha256=R6KjQ098eAFd2k5mKaSRJqMH1LhY4o7xkHrKT5vjRfs,1074
|
|
25
|
+
linked_data_python-0.6.1.dist-info/METADATA,sha256=SM4kVwA-gnxAZdihLeDPLR3Sys8n0iauEh40yw-8g_4,7974
|
|
26
|
+
linked_data_python-0.6.1.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
|
|
27
|
+
linked_data_python-0.6.1.dist-info/entry_points.txt,sha256=n8Vs52oH81BrKOe1DNUvdFLSOlEufRMpK2waPFa6aYk,224
|
|
28
|
+
linked_data_python-0.6.1.dist-info/top_level.txt,sha256=dkG_F4jYkh6CS6gdpQeV_gKW3yaJBvl1ukUu0zo2KA8,5
|
|
29
|
+
linked_data_python-0.6.1.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
{linked_data_python-0.5.2.dist-info → linked_data_python-0.6.1.dist-info}/licenses/LICENSE.md
RENAMED
|
File without changes
|
|
File without changes
|