linked-data-python 0.6.3__tar.gz → 0.6.5__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {linked_data_python-0.6.3 → linked_data_python-0.6.5}/PKG-INFO +1 -1
- {linked_data_python-0.6.3 → linked_data_python-0.6.5}/ldpy/runtime.py +42 -3
- {linked_data_python-0.6.3 → linked_data_python-0.6.5}/ldpy/sparql.py +41 -0
- {linked_data_python-0.6.3 → linked_data_python-0.6.5}/ldpy/transpiler/core.py +64 -1
- {linked_data_python-0.6.3 → linked_data_python-0.6.5}/linked_data_python.egg-info/PKG-INFO +1 -1
- {linked_data_python-0.6.3 → linked_data_python-0.6.5}/pyproject.toml +1 -1
- {linked_data_python-0.6.3 → linked_data_python-0.6.5}/tests/test_bindings.py +54 -0
- {linked_data_python-0.6.3 → linked_data_python-0.6.5}/tests/test_current_graph.py +52 -0
- {linked_data_python-0.6.3 → linked_data_python-0.6.5}/tests/test_debug_stepping.py +8 -1
- {linked_data_python-0.6.3 → linked_data_python-0.6.5}/tests/test_islands_terms.py +63 -0
- {linked_data_python-0.6.3 → linked_data_python-0.6.5}/tests/test_prefix_scoping.py +21 -0
- {linked_data_python-0.6.3 → linked_data_python-0.6.5}/tests/test_sparql_expr.py +32 -2
- {linked_data_python-0.6.3 → linked_data_python-0.6.5}/LICENSE.md +0 -0
- {linked_data_python-0.6.3 → linked_data_python-0.6.5}/README.md +0 -0
- {linked_data_python-0.6.3 → linked_data_python-0.6.5}/ldpy/__init__.py +0 -0
- {linked_data_python-0.6.3 → linked_data_python-0.6.5}/ldpy/__main__.py +0 -0
- {linked_data_python-0.6.3 → linked_data_python-0.6.5}/ldpy/backend.py +0 -0
- {linked_data_python-0.6.3 → linked_data_python-0.6.5}/ldpy/build.py +0 -0
- {linked_data_python-0.6.3 → linked_data_python-0.6.5}/ldpy/console.py +0 -0
- {linked_data_python-0.6.3 → linked_data_python-0.6.5}/ldpy/debug.py +0 -0
- {linked_data_python-0.6.3 → linked_data_python-0.6.5}/ldpy/formatter.py +0 -0
- {linked_data_python-0.6.3 → linked_data_python-0.6.5}/ldpy/importer.py +0 -0
- {linked_data_python-0.6.3 → linked_data_python-0.6.5}/ldpy/lsp/__init__.py +0 -0
- {linked_data_python-0.6.3 → linked_data_python-0.6.5}/ldpy/lsp/__main__.py +0 -0
- {linked_data_python-0.6.3 → linked_data_python-0.6.5}/ldpy/lsp/backend.py +0 -0
- {linked_data_python-0.6.3 → linked_data_python-0.6.5}/ldpy/lsp/hover.py +0 -0
- {linked_data_python-0.6.3 → linked_data_python-0.6.5}/ldpy/lsp/islanddoc.py +0 -0
- {linked_data_python-0.6.3 → linked_data_python-0.6.5}/ldpy/lsp/rpc.py +0 -0
- {linked_data_python-0.6.3 → linked_data_python-0.6.5}/ldpy/lsp/server.py +0 -0
- {linked_data_python-0.6.3 → linked_data_python-0.6.5}/ldpy/lsp/translate.py +0 -0
- {linked_data_python-0.6.3 → linked_data_python-0.6.5}/ldpy/pygments_lexer.py +0 -0
- {linked_data_python-0.6.3 → linked_data_python-0.6.5}/ldpy/transpiler/__init__.py +0 -0
- {linked_data_python-0.6.3 → linked_data_python-0.6.5}/ldpy/transpiler/errors.py +0 -0
- {linked_data_python-0.6.3 → linked_data_python-0.6.5}/ldpy/transpiler/linemap.py +0 -0
- {linked_data_python-0.6.3 → linked_data_python-0.6.5}/linked_data_python.egg-info/SOURCES.txt +0 -0
- {linked_data_python-0.6.3 → linked_data_python-0.6.5}/linked_data_python.egg-info/dependency_links.txt +0 -0
- {linked_data_python-0.6.3 → linked_data_python-0.6.5}/linked_data_python.egg-info/entry_points.txt +0 -0
- {linked_data_python-0.6.3 → linked_data_python-0.6.5}/linked_data_python.egg-info/requires.txt +0 -0
- {linked_data_python-0.6.3 → linked_data_python-0.6.5}/linked_data_python.egg-info/top_level.txt +0 -0
- {linked_data_python-0.6.3 → linked_data_python-0.6.5}/setup.cfg +0 -0
- {linked_data_python-0.6.3 → linked_data_python-0.6.5}/tests/test_bench.py +0 -0
- {linked_data_python-0.6.3 → linked_data_python-0.6.5}/tests/test_build.py +0 -0
- {linked_data_python-0.6.3 → linked_data_python-0.6.5}/tests/test_call_suffix.py +0 -0
- {linked_data_python-0.6.3 → linked_data_python-0.6.5}/tests/test_charsets.py +0 -0
- {linked_data_python-0.6.3 → linked_data_python-0.6.5}/tests/test_coercion.py +0 -0
- {linked_data_python-0.6.3 → linked_data_python-0.6.5}/tests/test_console.py +0 -0
- {linked_data_python-0.6.3 → linked_data_python-0.6.5}/tests/test_debug.py +0 -0
- {linked_data_python-0.6.3 → linked_data_python-0.6.5}/tests/test_disambiguation.py +0 -0
- {linked_data_python-0.6.3 → linked_data_python-0.6.5}/tests/test_docs.py +0 -0
- {linked_data_python-0.6.3 → linked_data_python-0.6.5}/tests/test_docstrings.py +0 -0
- {linked_data_python-0.6.3 → linked_data_python-0.6.5}/tests/test_examples.py +0 -0
- {linked_data_python-0.6.3 → linked_data_python-0.6.5}/tests/test_formatter.py +0 -0
- {linked_data_python-0.6.3 → linked_data_python-0.6.5}/tests/test_generator.py +0 -0
- {linked_data_python-0.6.3 → linked_data_python-0.6.5}/tests/test_graphs.py +0 -0
- {linked_data_python-0.6.3 → linked_data_python-0.6.5}/tests/test_hover_parts.py +0 -0
- {linked_data_python-0.6.3 → linked_data_python-0.6.5}/tests/test_identity.py +0 -0
- {linked_data_python-0.6.3 → linked_data_python-0.6.5}/tests/test_import_hook.py +0 -0
- {linked_data_python-0.6.3 → linked_data_python-0.6.5}/tests/test_islanddoc.py +0 -0
- {linked_data_python-0.6.3 → linked_data_python-0.6.5}/tests/test_lexing.py +0 -0
- {linked_data_python-0.6.3 → linked_data_python-0.6.5}/tests/test_linemap.py +0 -0
- {linked_data_python-0.6.3 → linked_data_python-0.6.5}/tests/test_lsp_rpc.py +0 -0
- {linked_data_python-0.6.3 → linked_data_python-0.6.5}/tests/test_lsp_server.py +0 -0
- {linked_data_python-0.6.3 → linked_data_python-0.6.5}/tests/test_lsp_translate.py +0 -0
- {linked_data_python-0.6.3 → linked_data_python-0.6.5}/tests/test_match_island.py +0 -0
- {linked_data_python-0.6.3 → linked_data_python-0.6.5}/tests/test_micropython_subset.py +0 -0
- {linked_data_python-0.6.3 → linked_data_python-0.6.5}/tests/test_prefix_import.py +0 -0
- {linked_data_python-0.6.3 → linked_data_python-0.6.5}/tests/test_pygments_lexer.py +0 -0
- {linked_data_python-0.6.3 → linked_data_python-0.6.5}/tests/test_scope_modifiers.py +0 -0
- {linked_data_python-0.6.3 → linked_data_python-0.6.5}/tests/test_semantics.py +0 -0
- {linked_data_python-0.6.3 → linked_data_python-0.6.5}/tests/test_sourcemap_v3.py +0 -0
- {linked_data_python-0.6.3 → linked_data_python-0.6.5}/tests/test_sparql_island.py +0 -0
- {linked_data_python-0.6.3 → linked_data_python-0.6.5}/tests/test_target_micropython.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: linked-data-python
|
|
3
|
-
Version: 0.6.
|
|
3
|
+
Version: 0.6.5
|
|
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
|
|
@@ -602,7 +602,21 @@ class Bindings(dict):
|
|
|
602
602
|
def as_bindings_iter(iterable):
|
|
603
603
|
"""'for @bindings in ...' (record ldpy/017): every element becomes the
|
|
604
604
|
current bindings of the body. An m{ ... } yields its solutions (anonymous
|
|
605
|
-
variables excluded); any other iterable must produce mappings
|
|
605
|
+
variables excluded); any other iterable must produce mappings or
|
|
606
|
+
namedtuple-like rows.
|
|
607
|
+
|
|
608
|
+
Two duck-typed shapes are accepted for a row, and both are needed because
|
|
609
|
+
CPython and MicroPython expose different halves of the namedtuple API:
|
|
610
|
+
|
|
611
|
+
- `_fields`, a sequence of names in value order — CPython's
|
|
612
|
+
`collections.namedtuple`, a `pandas.DataFrame.itertuples()` row, and
|
|
613
|
+
ldpy's own Row from `m{ }` (record ldpy/016);
|
|
614
|
+
- `_asdict()`, when `_fields` is absent — this is the ONLY one
|
|
615
|
+
MicroPython's `collections.namedtuple` exposes, and only when the
|
|
616
|
+
firmware was built with `MICROPY_PY_COLLECTIONS_NAMEDTUPLE__ASDICT`.
|
|
617
|
+
Keying off `_fields` alone would have left `for @bindings` broken on
|
|
618
|
+
device, in a file record ldpy/026 requires to emit the same code on
|
|
619
|
+
both backends."""
|
|
606
620
|
if isinstance(iterable, Match):
|
|
607
621
|
for sm in iterable.solutions():
|
|
608
622
|
b = Bindings()
|
|
@@ -617,10 +631,17 @@ def as_bindings_iter(iterable):
|
|
|
617
631
|
yield item
|
|
618
632
|
elif hasattr(item, "items"):
|
|
619
633
|
yield Bindings(item)
|
|
634
|
+
elif hasattr(item, "_fields"):
|
|
635
|
+
b = Bindings()
|
|
636
|
+
b.update(zip(item._fields, item))
|
|
637
|
+
yield b
|
|
638
|
+
elif hasattr(item, "_asdict"):
|
|
639
|
+
# MicroPython's namedtuple: no _fields, _asdict() instead.
|
|
640
|
+
yield Bindings(item._asdict())
|
|
620
641
|
else:
|
|
621
642
|
raise TypeError(
|
|
622
|
-
"for @bindings in ...: the iterable must produce mappings
|
|
623
|
-
"got %s" % type(item).__name__)
|
|
643
|
+
"for @bindings in ...: the iterable must produce mappings "
|
|
644
|
+
"or namedtuple-like rows, got %s" % type(item).__name__)
|
|
624
645
|
|
|
625
646
|
|
|
626
647
|
_EXPR_CLASS = None
|
|
@@ -732,6 +753,24 @@ def designate(namespaces, graph):
|
|
|
732
753
|
return graph
|
|
733
754
|
|
|
734
755
|
|
|
756
|
+
def bind_prefix(graph, prefix, ns):
|
|
757
|
+
"""'@prefix' declared while a graph is current — bind it there too.
|
|
758
|
+
|
|
759
|
+
:func:`designate` binds the prefixes in scope at the moment of
|
|
760
|
+
designation. A prefix declared AFTER, while that same graph is still the
|
|
761
|
+
current one, is in scope for it just as much: binding it here is what
|
|
762
|
+
makes the two orders serialise alike, instead of making the position of a
|
|
763
|
+
declaration a trap (record ldpy/027).
|
|
764
|
+
|
|
765
|
+
Same tolerance as :func:`designate`: anything that is not a graph, or a
|
|
766
|
+
backend without a namespace manager, passes through untouched.
|
|
767
|
+
"""
|
|
768
|
+
try:
|
|
769
|
+
_backend.bind_namespaces(graph, {prefix: ns})
|
|
770
|
+
except (AttributeError, TypeError):
|
|
771
|
+
pass # not a graph, or no binding support
|
|
772
|
+
|
|
773
|
+
|
|
735
774
|
def add_to(graph, *triples, bindings=None):
|
|
736
775
|
"""'+{ ... }': instantiate and add to the current graph (record ldpy/014).
|
|
737
776
|
A triple with a term still unbound is dropped — one cannot write an
|
|
@@ -360,6 +360,47 @@ def DATATYPE(t):
|
|
|
360
360
|
return t.datatype or XSD.string
|
|
361
361
|
|
|
362
362
|
|
|
363
|
+
def STRLANG(t, lang):
|
|
364
|
+
"""STRLANG(lexical form, language tag).
|
|
365
|
+
|
|
366
|
+
The way to give a bound variable a language tag: `?v@en` is refused —
|
|
367
|
+
a variable is already bound to a COMPLETE term, so there is nothing to
|
|
368
|
+
interpret (record ldpy/027) — whereas STRLANG says explicitly to take
|
|
369
|
+
the lexical form and read it as a tagged literal.
|
|
370
|
+
|
|
371
|
+
SPARQL 1.1 wants a plain literal with no tag and no datatype, and gives
|
|
372
|
+
no result otherwise; here that is an error, which is what the rest of
|
|
373
|
+
this module does with an argument of the wrong shape.
|
|
374
|
+
"""
|
|
375
|
+
if not isinstance(t, Literal):
|
|
376
|
+
raise SparqlError("STRLANG wants a literal as its first argument")
|
|
377
|
+
if t.language or (t.datatype and t.datatype != XSD.string):
|
|
378
|
+
raise SparqlError(
|
|
379
|
+
"STRLANG wants a plain literal: %r already carries a %s"
|
|
380
|
+
% (str(t), "language tag" if t.language else "datatype"))
|
|
381
|
+
tag = str(lang)
|
|
382
|
+
if not tag:
|
|
383
|
+
raise SparqlError("STRLANG wants a non-empty language tag")
|
|
384
|
+
return Literal(str(t), lang=tag)
|
|
385
|
+
|
|
386
|
+
|
|
387
|
+
def STRDT(t, dt):
|
|
388
|
+
"""STRDT(lexical form, datatype IRI).
|
|
389
|
+
|
|
390
|
+
The counterpart of :func:`STRLANG` for `?v^^dt`, refused for the same
|
|
391
|
+
reason (record ldpy/027).
|
|
392
|
+
"""
|
|
393
|
+
if not isinstance(t, Literal):
|
|
394
|
+
raise SparqlError("STRDT wants a literal as its first argument")
|
|
395
|
+
if t.language or (t.datatype and t.datatype != XSD.string):
|
|
396
|
+
raise SparqlError(
|
|
397
|
+
"STRDT wants a plain literal: %r already carries a %s"
|
|
398
|
+
% (str(t), "language tag" if t.language else "datatype"))
|
|
399
|
+
if not isinstance(dt, URIRef):
|
|
400
|
+
raise SparqlError("STRDT wants an IRI as its datatype")
|
|
401
|
+
return Literal(str(t), datatype=dt)
|
|
402
|
+
|
|
403
|
+
|
|
363
404
|
def IRI(t, base=None):
|
|
364
405
|
"""IRI(str) — resolved against the lexical base of the call site."""
|
|
365
406
|
s = str(t)
|
|
@@ -1356,9 +1356,25 @@ class Transpiler:
|
|
|
1356
1356
|
prefix, as_name, RUNTIME_ALIAS, resolved)
|
|
1357
1357
|
if scope_mode:
|
|
1358
1358
|
gen = "%s %s; %s" % (scope_mode, as_name, gen)
|
|
1359
|
+
if kind == "prefix":
|
|
1360
|
+
gen = self._bind_on_current_graph(gen, prefix)
|
|
1359
1361
|
self._end_island(kind, mark, gen)
|
|
1360
1362
|
return True
|
|
1361
1363
|
|
|
1364
|
+
def _bind_on_current_graph(self, gen, prefix):
|
|
1365
|
+
"""Lie aussi le préfixe sur le graphe courant, s'il y en a un.
|
|
1366
|
+
|
|
1367
|
+
`@graph` lie les préfixes en portée au moment de la désignation. Un
|
|
1368
|
+
préfixe déclaré APRÈS, tant que ce graphe reste courant, est en
|
|
1369
|
+
portée pour lui tout autant : le lier ici est ce qui fait que les
|
|
1370
|
+
deux ordres d'écriture sérialisent pareil, au lieu de faire de la
|
|
1371
|
+
position d'une déclaration un piège (fiche 027).
|
|
1372
|
+
"""
|
|
1373
|
+
if not self._graph_var:
|
|
1374
|
+
return gen
|
|
1375
|
+
return gen + "; %s.bind_prefix(%s, %r, __namespaces__[%r])" % (
|
|
1376
|
+
RUNTIME_ALIAS, self._graph_var, prefix, prefix)
|
|
1377
|
+
|
|
1362
1378
|
def _prefix_as_clause(self, k):
|
|
1363
1379
|
"""`as NAME` before a @prefix declaration's closing '.', or nothing.
|
|
1364
1380
|
|
|
@@ -1392,6 +1408,10 @@ class Transpiler:
|
|
|
1392
1408
|
self._take(j - self.i) # '@prefix p:' + blancs
|
|
1393
1409
|
parts = self._take_firi_parts()
|
|
1394
1410
|
self._g_ws()
|
|
1411
|
+
as_name, k = self._prefix_as_clause(self.i)
|
|
1412
|
+
if as_name:
|
|
1413
|
+
self._take(k - self.i)
|
|
1414
|
+
self._g_ws()
|
|
1395
1415
|
if self._peek() != ".":
|
|
1396
1416
|
self._error("'.' attendu pour clore la déclaration @prefix")
|
|
1397
1417
|
self._take(1)
|
|
@@ -1411,6 +1431,11 @@ class Transpiler:
|
|
|
1411
1431
|
scope_mode)
|
|
1412
1432
|
gen = "__namespaces__[%r] = %s.Namespace(%r)" % (
|
|
1413
1433
|
prefix, RUNTIME_ALIAS, resolved)
|
|
1434
|
+
if as_name:
|
|
1435
|
+
gen = "__namespaces__[%r] = %s = %s.Namespace(%r)" % (
|
|
1436
|
+
prefix, as_name, RUNTIME_ALIAS, resolved)
|
|
1437
|
+
if scope_mode:
|
|
1438
|
+
gen = "%s %s; %s" % (scope_mode, as_name, gen)
|
|
1414
1439
|
else:
|
|
1415
1440
|
# global/nonlocal : réutiliser la variable de la liaison cible si
|
|
1416
1441
|
# elle est dynamique — c'est elle que les usages du dehors lisent
|
|
@@ -1424,8 +1449,15 @@ class Transpiler:
|
|
|
1424
1449
|
gen = "%s = %s.Namespace(%s.firi(%s)); __namespaces__[%r] = %s" % (
|
|
1425
1450
|
var, RUNTIME_ALIAS, RUNTIME_ALIAS,
|
|
1426
1451
|
self._firi_args(parts), prefix, var)
|
|
1452
|
+
if as_name:
|
|
1453
|
+
# `as EX` on a DYNAMIC prefix binds the Namespace built from
|
|
1454
|
+
# the IRI resolved at that point — the same object the
|
|
1455
|
+
# prefix itself uses, so the two can never disagree.
|
|
1456
|
+
gen += "; %s = %s" % (as_name, var)
|
|
1427
1457
|
if scope_mode:
|
|
1428
|
-
|
|
1458
|
+
names = var if not as_name else "%s, %s" % (var, as_name)
|
|
1459
|
+
gen = "%s %s; %s" % (scope_mode, names, gen)
|
|
1460
|
+
gen = self._bind_on_current_graph(gen, prefix)
|
|
1429
1461
|
self._end_island("prefix", mark, gen)
|
|
1430
1462
|
return True
|
|
1431
1463
|
|
|
@@ -1965,10 +1997,36 @@ class Transpiler:
|
|
|
1965
1997
|
if not m:
|
|
1966
1998
|
self._error("nom de variable attendu après '%s'" % sigil)
|
|
1967
1999
|
name = self._take(len(m.group(0)))
|
|
2000
|
+
self._refuse_suffix_on_variable(sigil, name)
|
|
1968
2001
|
if self._match_vars is not None and name not in self._match_vars:
|
|
1969
2002
|
self._match_vars.append(name)
|
|
1970
2003
|
return "%s.Variable(%r)" % (RUNTIME_ALIAS, name)
|
|
1971
2004
|
|
|
2005
|
+
def _refuse_suffix_on_variable(self, sigil, name):
|
|
2006
|
+
"""`?v@en` and `?v^^xsd:int` are refused, and say why.
|
|
2007
|
+
|
|
2008
|
+
Sticking an RDF suffix on an *interpolation* is meaningful — the
|
|
2009
|
+
Python value is the lexical form, and the suffix says how to read
|
|
2010
|
+
it. Sticking one on a variable is not: a variable is already bound
|
|
2011
|
+
to a complete term, and there is nothing left to interpret. Record
|
|
2012
|
+
027 closes the question — the language refuses rather than coerces
|
|
2013
|
+
— so the refusal has to name itself; before this, the parser only
|
|
2014
|
+
complained that the island's closing brace was missing."""
|
|
2015
|
+
c = self._peek()
|
|
2016
|
+
m = _LANGTAG_RE.match(self.text, self.i)
|
|
2017
|
+
if c == "@" and m:
|
|
2018
|
+
what, written, advice = "de langue", m.group(0), "@en"
|
|
2019
|
+
elif c == "^" and self._peek(1) == "^":
|
|
2020
|
+
what, written, advice = "de type", "^^...", "^^xsd:string"
|
|
2021
|
+
else:
|
|
2022
|
+
return
|
|
2023
|
+
self._error(
|
|
2024
|
+
"suffixe %s interdit sur une variable : '%s%s%s' — une variable "
|
|
2025
|
+
"est déjà liée à un terme complet, il n'y a rien à interpréter. "
|
|
2026
|
+
"Pour construire un littéral, coller le suffixe à une "
|
|
2027
|
+
"interpolation : '{expr}%s' (fiche 027)"
|
|
2028
|
+
% (what, sigil, name, written, advice))
|
|
2029
|
+
|
|
1972
2030
|
def _g_node(self, triples, gctx):
|
|
1973
2031
|
"""Records the term as a `part` of the island, then parses it.
|
|
1974
2032
|
|
|
@@ -2320,6 +2378,11 @@ class Transpiler:
|
|
|
2320
2378
|
"STRBEFORE", "STRAFTER", "REPLACE", "REGEX", "ABS", "ROUND", "CEIL",
|
|
2321
2379
|
"FLOOR", "SAMETERM", "ISIRI", "ISURI", "ISBLANK", "ISLITERAL",
|
|
2322
2380
|
"ISNUMERIC", "LANGMATCHES", "ENCODE_FOR_IRI",
|
|
2381
|
+
# STRLANG / STRDT : la façon de dire explicitement ce que `?v@en` et
|
|
2382
|
+
# `?v^^dt` ne disent pas — une variable est liée à un terme complet,
|
|
2383
|
+
# le suffixe est donc refusé (fiche 027) et ces deux-ci sont le
|
|
2384
|
+
# chemin propre, en SPARQL standard.
|
|
2385
|
+
"STRLANG", "STRDT",
|
|
2323
2386
|
))
|
|
2324
2387
|
|
|
2325
2388
|
def _take_enode(self):
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: linked-data-python
|
|
3
|
-
Version: 0.6.
|
|
3
|
+
Version: 0.6.5
|
|
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
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "linked-data-python"
|
|
7
|
-
version = "0.6.
|
|
7
|
+
version = "0.6.5"
|
|
8
8
|
description = "Python extended with Semantic Web primitives: IRIs, RDF literals and Turtle-notation graphs as expressions, transpiled by island parsing."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = { text = "MIT" }
|
|
@@ -213,6 +213,60 @@ def test_for_bindings_nested_masking(run):
|
|
|
213
213
|
assert g["a"] == Literal(2) and g["b"] == Literal(1)
|
|
214
214
|
|
|
215
215
|
|
|
216
|
+
def test_for_bindings_over_namedtuples(run):
|
|
217
|
+
g, _ = run(textwrap.dedent("""\
|
|
218
|
+
from collections import namedtuple
|
|
219
|
+
@prefix ex: <http://e/> .
|
|
220
|
+
@graph as g
|
|
221
|
+
Row = namedtuple("Row", ["id", "value"])
|
|
222
|
+
rows = [Row("a", 1), Row("b", 2)]
|
|
223
|
+
for @bindings in rows:
|
|
224
|
+
+{ ex:{?id} ex:value ?value }
|
|
225
|
+
n = len(g)
|
|
226
|
+
"""))
|
|
227
|
+
assert g["n"] == 2
|
|
228
|
+
assert (URIRef("http://e/a"), URIRef("http://e/value"), Literal(1)) in g["g"]
|
|
229
|
+
assert (URIRef("http://e/b"), URIRef("http://e/value"), Literal(2)) in g["g"]
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def test_for_bindings_over_rows_of_match(run):
|
|
233
|
+
g, _ = run(textwrap.dedent("""\
|
|
234
|
+
@prefix ex: <http://e/> .
|
|
235
|
+
@graph as src
|
|
236
|
+
+{ ex:c1 ex:reading 10 . ex:c2 ex:reading 25 }
|
|
237
|
+
@graph as out
|
|
238
|
+
rows = list(m{ ?s ex:reading ?v }(src))
|
|
239
|
+
for @bindings in rows:
|
|
240
|
+
+{ ?s ex:hasValue e{ ?v * 2 } }
|
|
241
|
+
vals = sorted(int(o) for s, o in m{ ?s ex:hasValue ?o }(out))
|
|
242
|
+
"""))
|
|
243
|
+
assert g["vals"] == [20, 50]
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def test_for_bindings_over_a_micropython_style_row(run):
|
|
247
|
+
"""MicroPython's `collections.namedtuple` exposes `_asdict()` and NOT
|
|
248
|
+
`_fields` — the reverse of CPython. `runtime.py` emits the same code on
|
|
249
|
+
both backends (record ldpy/026), so the row shape of the smaller one has
|
|
250
|
+
to be accepted too, or `for @bindings` is broken on device."""
|
|
251
|
+
g, _ = run(textwrap.dedent("""\
|
|
252
|
+
@prefix ex: <http://e/> .
|
|
253
|
+
@graph as g
|
|
254
|
+
|
|
255
|
+
class URow: # ce que voit MicroPython
|
|
256
|
+
def __init__(self, **kw):
|
|
257
|
+
self._d = kw
|
|
258
|
+
def _asdict(self):
|
|
259
|
+
return dict(self._d)
|
|
260
|
+
|
|
261
|
+
rows = [URow(id="a", value=1), URow(id="b", value=2)]
|
|
262
|
+
for @bindings in rows:
|
|
263
|
+
+{ ex:{?id} ex:value ?value }
|
|
264
|
+
n = len(g)
|
|
265
|
+
"""))
|
|
266
|
+
assert g["n"] == 2
|
|
267
|
+
assert (URIRef("http://e/a"), URIRef("http://e/value"), Literal(1)) in g["g"]
|
|
268
|
+
|
|
269
|
+
|
|
216
270
|
def test_for_bindings_non_mapping_raises(run):
|
|
217
271
|
g, r = run("collected = []\n")
|
|
218
272
|
src = "for @bindings in [1]:\n pass\n"
|
|
@@ -320,6 +320,58 @@ def test_created_and_designated_serialise_alike(run):
|
|
|
320
320
|
assert "@prefix ex:" in ns["given"].serialize(format="turtle")
|
|
321
321
|
|
|
322
322
|
|
|
323
|
+
def test_a_prefix_declared_after_the_designation_binds_too(run):
|
|
324
|
+
"""The position of a declaration is not a trap: as long as the graph is
|
|
325
|
+
the current one, a prefix declared after `@graph` is in scope for it and
|
|
326
|
+
binds there, exactly like those declared before."""
|
|
327
|
+
ns, _ = run("from rdflib import Graph\n"
|
|
328
|
+
"before = 1\n"
|
|
329
|
+
"@prefix a: <http://a/> .\n"
|
|
330
|
+
"mine = Graph()\n"
|
|
331
|
+
"@graph mine\n"
|
|
332
|
+
"@prefix b: <http://b/> .\n"
|
|
333
|
+
"+{ a:s a:p a:o . b:s b:p b:o }\n")
|
|
334
|
+
got = {p for p, _ in ns["mine"].namespaces()}
|
|
335
|
+
assert "a" in got and "b" in got
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
def test_both_declaration_orders_serialise_alike(run):
|
|
339
|
+
"""The reason the previous test matters: swapping the two lines must not
|
|
340
|
+
change what comes out of the serialiser."""
|
|
341
|
+
src = ("from rdflib import Graph\n"
|
|
342
|
+
"mine = Graph()\n"
|
|
343
|
+
"%s"
|
|
344
|
+
"+{ late:s late:p late:o }\n")
|
|
345
|
+
ns_before, _ = run(src % ("@prefix late: <http://late/> .\n"
|
|
346
|
+
"@graph mine\n"))
|
|
347
|
+
ns_after, _ = run(src % ("@graph mine\n"
|
|
348
|
+
"@prefix late: <http://late/> .\n"))
|
|
349
|
+
assert (ns_before["mine"].serialize(format="turtle")
|
|
350
|
+
== ns_after["mine"].serialize(format="turtle"))
|
|
351
|
+
|
|
352
|
+
|
|
353
|
+
def test_a_dynamic_prefix_declared_after_the_designation_binds_too(run):
|
|
354
|
+
"""Same rule for the f-IRI form, which goes through its own emission
|
|
355
|
+
path and would otherwise drift from the static one."""
|
|
356
|
+
ns, _ = run("from rdflib import Graph\n"
|
|
357
|
+
"mine = Graph()\n"
|
|
358
|
+
"host = 'example.org'\n"
|
|
359
|
+
"@graph mine\n"
|
|
360
|
+
"@prefix dyn: f<http://{host}/ns#> .\n"
|
|
361
|
+
"+{ dyn:a dyn:p 1 }\n")
|
|
362
|
+
assert ("dyn", URIRef("http://example.org/ns#")) in list(
|
|
363
|
+
ns["mine"].namespaces())
|
|
364
|
+
|
|
365
|
+
|
|
366
|
+
def test_no_current_graph_means_no_binding_call(run):
|
|
367
|
+
"""A prefix declared with no graph in scope emits nothing extra — the
|
|
368
|
+
check is on the generated code, so the common case stays untouched."""
|
|
369
|
+
code = T("""
|
|
370
|
+
@prefix ex: <http://example.org/ns#> .
|
|
371
|
+
""").code
|
|
372
|
+
assert "bind_prefix" not in code
|
|
373
|
+
|
|
374
|
+
|
|
323
375
|
def test_designating_keeps_bindings_the_caller_already_had(run):
|
|
324
376
|
"""A designated graph belongs to its caller: the block's prefixes are
|
|
325
377
|
ADDED to its own manager, never swapped for a shared one."""
|
|
@@ -220,7 +220,14 @@ def test_pas_entrant_dans_une_fonction_ldpy(traces):
|
|
|
220
220
|
"""`step in` sur `build(3)` entre dans le corps ldpy de la fonction, et
|
|
221
221
|
`step out` en ressort — le tout en coordonnées .ldpy."""
|
|
222
222
|
stops = traces["fonction"]
|
|
223
|
-
|
|
223
|
+
lignes = [st.line for st in stops][:3]
|
|
224
|
+
assert lignes[:2] == [4, 3]
|
|
225
|
+
# Sortir de `build` rend la main SUR la ligne d'appel (4) à partir de
|
|
226
|
+
# CPython 3.11 ; sur 3.10 la ligne courante a déjà avancé (5). Les deux
|
|
227
|
+
# sont des arrêts sur une région .ldpy et le geste a bien changé de
|
|
228
|
+
# région : l'invariant de la fiche 103 tient des deux côtés. Ce qui est
|
|
229
|
+
# propre à ldpy, et donc ce que ce test doit serrer, c'est la PROFONDEUR.
|
|
230
|
+
assert lignes[2] in (4, 5)
|
|
224
231
|
assert stops[1].depth == stops[0].depth + 1
|
|
225
232
|
assert stops[2].depth == stops[0].depth
|
|
226
233
|
|
|
@@ -182,3 +182,66 @@ def test_datatype_interpolation_unclosed_is_an_error(run, prefixes):
|
|
|
182
182
|
from ldpy.transpiler import LdpySyntaxError
|
|
183
183
|
with pytest.raises(LdpySyntaxError):
|
|
184
184
|
run(prefixes + "dt = xsd:date\nx = \"1\"^^{dt\n")
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
# --------------------------------------------------------------- record 027
|
|
188
|
+
# `{expr}@en` is meaningful — the Python value is the lexical form and the
|
|
189
|
+
# suffix says how to read it. `?v@en` is not: a variable is already bound to
|
|
190
|
+
# a complete term. Maxime closed the question on 2026-09-03 (refuse, do not
|
|
191
|
+
# coerce); what this checks is that the refusal names itself, since before it
|
|
192
|
+
# the parser only complained about a missing closing brace.
|
|
193
|
+
|
|
194
|
+
def test_language_suffix_on_a_variable_is_refused(prefixes):
|
|
195
|
+
import pytest
|
|
196
|
+
from ldpy.transpiler import LdpySyntaxError
|
|
197
|
+
with pytest.raises(LdpySyntaxError) as exc:
|
|
198
|
+
transpile(prefixes + "@graph as g\n+{ ex:s ex:p ?v@fr-BE }\n")
|
|
199
|
+
assert "?v@fr-BE" in str(exc.value)
|
|
200
|
+
assert "{expr}@en" in str(exc.value)
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def test_datatype_suffix_on_a_variable_is_refused(prefixes):
|
|
204
|
+
import pytest
|
|
205
|
+
from ldpy.transpiler import LdpySyntaxError
|
|
206
|
+
with pytest.raises(LdpySyntaxError) as exc:
|
|
207
|
+
transpile(prefixes + "@graph as g\n+{ ex:s ex:p ?v^^xsd:integer }\n")
|
|
208
|
+
assert "suffixe de type" in str(exc.value)
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def test_the_refusal_reaches_every_island_that_binds_variables(prefixes):
|
|
212
|
+
import pytest
|
|
213
|
+
from ldpy.transpiler import LdpySyntaxError
|
|
214
|
+
for island in ("+{ ex:s ex:p ?v@en }", "-{ ex:s ex:p ?v@en }",
|
|
215
|
+
"for b in m{ ?s ex:p ?v@en }: pass"):
|
|
216
|
+
with pytest.raises(LdpySyntaxError):
|
|
217
|
+
transpile(prefixes + "@graph as g\n" + island + "\n")
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def test_strlang_is_the_way_the_refusal_leaves_open(run, prefixes):
|
|
221
|
+
"""The refusal above is only defensible if saying it explicitly is
|
|
222
|
+
possible. STRLANG and STRDT are that way, in `+{ }` and `-{ }`; they need
|
|
223
|
+
a `@bindings` in scope, like any `e{ }` used as a term there."""
|
|
224
|
+
g, _ = run(prefixes + "@graph as g\n@bindings as b\n"
|
|
225
|
+
'+{ ex:s ex:p e{ STRLANG("bonjour", "en") } }\n'
|
|
226
|
+
'+{ ex:s ex:q e{ STRDT("42", xsd:integer) } }\n')
|
|
227
|
+
got = {t[2] for t in g["g"]}
|
|
228
|
+
assert Literal("bonjour", lang="en") in got
|
|
229
|
+
assert Literal("42", datatype=XSD.integer) in got
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def test_strlang_in_a_match_island_stays_refused(prefixes):
|
|
233
|
+
"""`e{ }` as a matching filter is out of scope by decision, not by
|
|
234
|
+
omission (record ldpy/017) — adding STRLANG did not open that door."""
|
|
235
|
+
import pytest
|
|
236
|
+
from ldpy.transpiler import LdpySyntaxError
|
|
237
|
+
with pytest.raises(LdpySyntaxError) as exc:
|
|
238
|
+
transpile(prefixes + "@graph as g\n"
|
|
239
|
+
'for b in m{ ?s ex:p e{ STRLANG(?v, "en") } }: pass\n')
|
|
240
|
+
assert "017" in str(exc.value) or "hors périmètre" in str(exc.value)
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
def test_a_bare_variable_and_a_suffixed_interpolation_still_work(run, prefixes):
|
|
244
|
+
g, _ = run(prefixes + "@graph as g\nv = 'x'\n"
|
|
245
|
+
"+{ ex:s ex:p {v}@en }\n"
|
|
246
|
+
"-{ ex:s ex:q ?any }\n")
|
|
247
|
+
assert list(g["g"])[0][2] == Literal("x", lang="en")
|
|
@@ -347,3 +347,24 @@ def test_a_prefix_named_as_something_is_not_an_as_clause(run):
|
|
|
347
347
|
ns, _ = run("@prefix ex: <http://example.org/ns#> as assets .\n"
|
|
348
348
|
"t = ex:Thing\n")
|
|
349
349
|
assert str(ns["assets"]) == "http://example.org/ns#"
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
def test_as_works_on_a_dynamic_prefix_too(run):
|
|
353
|
+
"""`@prefix p: f<…> as P .` — the form the corpus actually needed first.
|
|
354
|
+
Every IRI of the region that motivated `as` is computed at run time, so
|
|
355
|
+
`as` on the static form alone would not have covered its own use case."""
|
|
356
|
+
ns, _ = run("host = 'example.org'\n"
|
|
357
|
+
"@prefix dyn: f<http://{host}/ns#> as DYN .\n"
|
|
358
|
+
"a = DYN['x']\n"
|
|
359
|
+
"b = dyn:x\n")
|
|
360
|
+
assert str(ns["DYN"]) == "http://example.org/ns#"
|
|
361
|
+
assert ns["a"] == ns["b"]
|
|
362
|
+
|
|
363
|
+
|
|
364
|
+
def test_a_dynamic_as_name_follows_the_scope_modifier(run):
|
|
365
|
+
ns, _ = run("host = 'example.org'\n"
|
|
366
|
+
"def f():\n"
|
|
367
|
+
" global @prefix dyn: f<http://{host}/ns#> as DYN .\n"
|
|
368
|
+
"f()\n"
|
|
369
|
+
"seen = str(DYN)\n")
|
|
370
|
+
assert ns["seen"] == "http://example.org/ns#"
|
|
@@ -12,9 +12,10 @@ from ldpy.sparql import SparqlError
|
|
|
12
12
|
P = "@prefix ex: <http://example.org/ns#> .\n"
|
|
13
13
|
|
|
14
14
|
|
|
15
|
-
def make(src):
|
|
15
|
+
def make(src, prefixes=""):
|
|
16
16
|
g = {"__name__": "t"}
|
|
17
|
-
exec(compile(transpile(P + src, "<t>").code, "<t>", "exec"),
|
|
17
|
+
exec(compile(transpile(P + prefixes + src, "<t>").code, "<t>", "exec"),
|
|
18
|
+
g)
|
|
18
19
|
return g
|
|
19
20
|
|
|
20
21
|
|
|
@@ -155,6 +156,35 @@ def test_lang_builtins():
|
|
|
155
156
|
assert g["m"].ebv(v=v) is True
|
|
156
157
|
|
|
157
158
|
|
|
159
|
+
def test_strlang_and_strdt_build_the_term_a_suffix_cannot():
|
|
160
|
+
"""`?v@en` and `?v^^dt` are refused — a variable is already bound to a
|
|
161
|
+
complete term (record ldpy/027). STRLANG and STRDT are the explicit way
|
|
162
|
+
to say what the suffix cannot, and they are standard SPARQL 1.1."""
|
|
163
|
+
g = make('a = e{ STRLANG(?v, "en") }\n'
|
|
164
|
+
"b = e{ STRDT(?v, xsd:integer) }\n",
|
|
165
|
+
prefixes='@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .\n')
|
|
166
|
+
assert g["a"](v=Literal("hello")) == Literal("hello", lang="en")
|
|
167
|
+
assert g["b"](v=Literal("42")) == Literal("42", datatype=XSD.integer)
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def test_strlang_and_strdt_refuse_a_term_that_is_already_complete():
|
|
171
|
+
"""SPARQL 1.1 gives no result for an argument that already carries a tag
|
|
172
|
+
or a datatype; here that is an error, like the rest of this module."""
|
|
173
|
+
g = make('a = e{ STRLANG(?v, "en") }\nb = e{ STRDT(?v, ex:dt) }\n')
|
|
174
|
+
with pytest.raises(SparqlError):
|
|
175
|
+
g["a"](v=Literal("hello", lang="fr"))
|
|
176
|
+
with pytest.raises(SparqlError):
|
|
177
|
+
g["b"](v=Literal("42", datatype=XSD.integer))
|
|
178
|
+
with pytest.raises(SparqlError):
|
|
179
|
+
g["a"](v=URIRef("http://example.org/ns#s"))
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def test_strlang_refuses_an_empty_tag():
|
|
183
|
+
g = make('a = e{ STRLANG(?v, ?t) }\n')
|
|
184
|
+
with pytest.raises(SparqlError):
|
|
185
|
+
g["a"](v=Literal("hello"), t=Literal(""))
|
|
186
|
+
|
|
187
|
+
|
|
158
188
|
def test_numeric_builtins():
|
|
159
189
|
g = make("r = e{ ROUND(?x) + CEIL(?y) + FLOOR(?y) + ABS(-?x) }\n")
|
|
160
190
|
assert g["r"](x=2.5, y=1.2) == Literal(2.5 + 0.5 + 2 + 1 + 2.5 - 0.5) \
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{linked_data_python-0.6.3 → linked_data_python-0.6.5}/linked_data_python.egg-info/SOURCES.txt
RENAMED
|
File without changes
|
|
File without changes
|
{linked_data_python-0.6.3 → linked_data_python-0.6.5}/linked_data_python.egg-info/entry_points.txt
RENAMED
|
File without changes
|
{linked_data_python-0.6.3 → linked_data_python-0.6.5}/linked_data_python.egg-info/requires.txt
RENAMED
|
File without changes
|
{linked_data_python-0.6.3 → linked_data_python-0.6.5}/linked_data_python.egg-info/top_level.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|