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.
Files changed (72) hide show
  1. {linked_data_python-0.6.3 → linked_data_python-0.6.5}/PKG-INFO +1 -1
  2. {linked_data_python-0.6.3 → linked_data_python-0.6.5}/ldpy/runtime.py +42 -3
  3. {linked_data_python-0.6.3 → linked_data_python-0.6.5}/ldpy/sparql.py +41 -0
  4. {linked_data_python-0.6.3 → linked_data_python-0.6.5}/ldpy/transpiler/core.py +64 -1
  5. {linked_data_python-0.6.3 → linked_data_python-0.6.5}/linked_data_python.egg-info/PKG-INFO +1 -1
  6. {linked_data_python-0.6.3 → linked_data_python-0.6.5}/pyproject.toml +1 -1
  7. {linked_data_python-0.6.3 → linked_data_python-0.6.5}/tests/test_bindings.py +54 -0
  8. {linked_data_python-0.6.3 → linked_data_python-0.6.5}/tests/test_current_graph.py +52 -0
  9. {linked_data_python-0.6.3 → linked_data_python-0.6.5}/tests/test_debug_stepping.py +8 -1
  10. {linked_data_python-0.6.3 → linked_data_python-0.6.5}/tests/test_islands_terms.py +63 -0
  11. {linked_data_python-0.6.3 → linked_data_python-0.6.5}/tests/test_prefix_scoping.py +21 -0
  12. {linked_data_python-0.6.3 → linked_data_python-0.6.5}/tests/test_sparql_expr.py +32 -2
  13. {linked_data_python-0.6.3 → linked_data_python-0.6.5}/LICENSE.md +0 -0
  14. {linked_data_python-0.6.3 → linked_data_python-0.6.5}/README.md +0 -0
  15. {linked_data_python-0.6.3 → linked_data_python-0.6.5}/ldpy/__init__.py +0 -0
  16. {linked_data_python-0.6.3 → linked_data_python-0.6.5}/ldpy/__main__.py +0 -0
  17. {linked_data_python-0.6.3 → linked_data_python-0.6.5}/ldpy/backend.py +0 -0
  18. {linked_data_python-0.6.3 → linked_data_python-0.6.5}/ldpy/build.py +0 -0
  19. {linked_data_python-0.6.3 → linked_data_python-0.6.5}/ldpy/console.py +0 -0
  20. {linked_data_python-0.6.3 → linked_data_python-0.6.5}/ldpy/debug.py +0 -0
  21. {linked_data_python-0.6.3 → linked_data_python-0.6.5}/ldpy/formatter.py +0 -0
  22. {linked_data_python-0.6.3 → linked_data_python-0.6.5}/ldpy/importer.py +0 -0
  23. {linked_data_python-0.6.3 → linked_data_python-0.6.5}/ldpy/lsp/__init__.py +0 -0
  24. {linked_data_python-0.6.3 → linked_data_python-0.6.5}/ldpy/lsp/__main__.py +0 -0
  25. {linked_data_python-0.6.3 → linked_data_python-0.6.5}/ldpy/lsp/backend.py +0 -0
  26. {linked_data_python-0.6.3 → linked_data_python-0.6.5}/ldpy/lsp/hover.py +0 -0
  27. {linked_data_python-0.6.3 → linked_data_python-0.6.5}/ldpy/lsp/islanddoc.py +0 -0
  28. {linked_data_python-0.6.3 → linked_data_python-0.6.5}/ldpy/lsp/rpc.py +0 -0
  29. {linked_data_python-0.6.3 → linked_data_python-0.6.5}/ldpy/lsp/server.py +0 -0
  30. {linked_data_python-0.6.3 → linked_data_python-0.6.5}/ldpy/lsp/translate.py +0 -0
  31. {linked_data_python-0.6.3 → linked_data_python-0.6.5}/ldpy/pygments_lexer.py +0 -0
  32. {linked_data_python-0.6.3 → linked_data_python-0.6.5}/ldpy/transpiler/__init__.py +0 -0
  33. {linked_data_python-0.6.3 → linked_data_python-0.6.5}/ldpy/transpiler/errors.py +0 -0
  34. {linked_data_python-0.6.3 → linked_data_python-0.6.5}/ldpy/transpiler/linemap.py +0 -0
  35. {linked_data_python-0.6.3 → linked_data_python-0.6.5}/linked_data_python.egg-info/SOURCES.txt +0 -0
  36. {linked_data_python-0.6.3 → linked_data_python-0.6.5}/linked_data_python.egg-info/dependency_links.txt +0 -0
  37. {linked_data_python-0.6.3 → linked_data_python-0.6.5}/linked_data_python.egg-info/entry_points.txt +0 -0
  38. {linked_data_python-0.6.3 → linked_data_python-0.6.5}/linked_data_python.egg-info/requires.txt +0 -0
  39. {linked_data_python-0.6.3 → linked_data_python-0.6.5}/linked_data_python.egg-info/top_level.txt +0 -0
  40. {linked_data_python-0.6.3 → linked_data_python-0.6.5}/setup.cfg +0 -0
  41. {linked_data_python-0.6.3 → linked_data_python-0.6.5}/tests/test_bench.py +0 -0
  42. {linked_data_python-0.6.3 → linked_data_python-0.6.5}/tests/test_build.py +0 -0
  43. {linked_data_python-0.6.3 → linked_data_python-0.6.5}/tests/test_call_suffix.py +0 -0
  44. {linked_data_python-0.6.3 → linked_data_python-0.6.5}/tests/test_charsets.py +0 -0
  45. {linked_data_python-0.6.3 → linked_data_python-0.6.5}/tests/test_coercion.py +0 -0
  46. {linked_data_python-0.6.3 → linked_data_python-0.6.5}/tests/test_console.py +0 -0
  47. {linked_data_python-0.6.3 → linked_data_python-0.6.5}/tests/test_debug.py +0 -0
  48. {linked_data_python-0.6.3 → linked_data_python-0.6.5}/tests/test_disambiguation.py +0 -0
  49. {linked_data_python-0.6.3 → linked_data_python-0.6.5}/tests/test_docs.py +0 -0
  50. {linked_data_python-0.6.3 → linked_data_python-0.6.5}/tests/test_docstrings.py +0 -0
  51. {linked_data_python-0.6.3 → linked_data_python-0.6.5}/tests/test_examples.py +0 -0
  52. {linked_data_python-0.6.3 → linked_data_python-0.6.5}/tests/test_formatter.py +0 -0
  53. {linked_data_python-0.6.3 → linked_data_python-0.6.5}/tests/test_generator.py +0 -0
  54. {linked_data_python-0.6.3 → linked_data_python-0.6.5}/tests/test_graphs.py +0 -0
  55. {linked_data_python-0.6.3 → linked_data_python-0.6.5}/tests/test_hover_parts.py +0 -0
  56. {linked_data_python-0.6.3 → linked_data_python-0.6.5}/tests/test_identity.py +0 -0
  57. {linked_data_python-0.6.3 → linked_data_python-0.6.5}/tests/test_import_hook.py +0 -0
  58. {linked_data_python-0.6.3 → linked_data_python-0.6.5}/tests/test_islanddoc.py +0 -0
  59. {linked_data_python-0.6.3 → linked_data_python-0.6.5}/tests/test_lexing.py +0 -0
  60. {linked_data_python-0.6.3 → linked_data_python-0.6.5}/tests/test_linemap.py +0 -0
  61. {linked_data_python-0.6.3 → linked_data_python-0.6.5}/tests/test_lsp_rpc.py +0 -0
  62. {linked_data_python-0.6.3 → linked_data_python-0.6.5}/tests/test_lsp_server.py +0 -0
  63. {linked_data_python-0.6.3 → linked_data_python-0.6.5}/tests/test_lsp_translate.py +0 -0
  64. {linked_data_python-0.6.3 → linked_data_python-0.6.5}/tests/test_match_island.py +0 -0
  65. {linked_data_python-0.6.3 → linked_data_python-0.6.5}/tests/test_micropython_subset.py +0 -0
  66. {linked_data_python-0.6.3 → linked_data_python-0.6.5}/tests/test_prefix_import.py +0 -0
  67. {linked_data_python-0.6.3 → linked_data_python-0.6.5}/tests/test_pygments_lexer.py +0 -0
  68. {linked_data_python-0.6.3 → linked_data_python-0.6.5}/tests/test_scope_modifiers.py +0 -0
  69. {linked_data_python-0.6.3 → linked_data_python-0.6.5}/tests/test_semantics.py +0 -0
  70. {linked_data_python-0.6.3 → linked_data_python-0.6.5}/tests/test_sourcemap_v3.py +0 -0
  71. {linked_data_python-0.6.3 → linked_data_python-0.6.5}/tests/test_sparql_island.py +0 -0
  72. {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
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
- gen = "%s %s; %s" % (scope_mode, var, gen)
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
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.3"
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
- assert [st.line for st in stops][:3] == [4, 3, 4]
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"), g)
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) \