linked-data-python 0.6.2__tar.gz → 0.6.4__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.2 → linked_data_python-0.6.4}/PKG-INFO +1 -1
  2. {linked_data_python-0.6.2 → linked_data_python-0.6.4}/ldpy/lsp/islanddoc.py +9 -5
  3. {linked_data_python-0.6.2 → linked_data_python-0.6.4}/ldpy/pygments_lexer.py +20 -0
  4. {linked_data_python-0.6.2 → linked_data_python-0.6.4}/ldpy/runtime.py +24 -0
  5. {linked_data_python-0.6.2 → linked_data_python-0.6.4}/ldpy/transpiler/core.py +52 -2
  6. {linked_data_python-0.6.2 → linked_data_python-0.6.4}/linked_data_python.egg-info/PKG-INFO +1 -1
  7. {linked_data_python-0.6.2 → linked_data_python-0.6.4}/pyproject.toml +1 -1
  8. {linked_data_python-0.6.2 → linked_data_python-0.6.4}/tests/test_current_graph.py +43 -0
  9. {linked_data_python-0.6.2 → linked_data_python-0.6.4}/tests/test_prefix_scoping.py +68 -0
  10. {linked_data_python-0.6.2 → linked_data_python-0.6.4}/LICENSE.md +0 -0
  11. {linked_data_python-0.6.2 → linked_data_python-0.6.4}/README.md +0 -0
  12. {linked_data_python-0.6.2 → linked_data_python-0.6.4}/ldpy/__init__.py +0 -0
  13. {linked_data_python-0.6.2 → linked_data_python-0.6.4}/ldpy/__main__.py +0 -0
  14. {linked_data_python-0.6.2 → linked_data_python-0.6.4}/ldpy/backend.py +0 -0
  15. {linked_data_python-0.6.2 → linked_data_python-0.6.4}/ldpy/build.py +0 -0
  16. {linked_data_python-0.6.2 → linked_data_python-0.6.4}/ldpy/console.py +0 -0
  17. {linked_data_python-0.6.2 → linked_data_python-0.6.4}/ldpy/debug.py +0 -0
  18. {linked_data_python-0.6.2 → linked_data_python-0.6.4}/ldpy/formatter.py +0 -0
  19. {linked_data_python-0.6.2 → linked_data_python-0.6.4}/ldpy/importer.py +0 -0
  20. {linked_data_python-0.6.2 → linked_data_python-0.6.4}/ldpy/lsp/__init__.py +0 -0
  21. {linked_data_python-0.6.2 → linked_data_python-0.6.4}/ldpy/lsp/__main__.py +0 -0
  22. {linked_data_python-0.6.2 → linked_data_python-0.6.4}/ldpy/lsp/backend.py +0 -0
  23. {linked_data_python-0.6.2 → linked_data_python-0.6.4}/ldpy/lsp/hover.py +0 -0
  24. {linked_data_python-0.6.2 → linked_data_python-0.6.4}/ldpy/lsp/rpc.py +0 -0
  25. {linked_data_python-0.6.2 → linked_data_python-0.6.4}/ldpy/lsp/server.py +0 -0
  26. {linked_data_python-0.6.2 → linked_data_python-0.6.4}/ldpy/lsp/translate.py +0 -0
  27. {linked_data_python-0.6.2 → linked_data_python-0.6.4}/ldpy/sparql.py +0 -0
  28. {linked_data_python-0.6.2 → linked_data_python-0.6.4}/ldpy/transpiler/__init__.py +0 -0
  29. {linked_data_python-0.6.2 → linked_data_python-0.6.4}/ldpy/transpiler/errors.py +0 -0
  30. {linked_data_python-0.6.2 → linked_data_python-0.6.4}/ldpy/transpiler/linemap.py +0 -0
  31. {linked_data_python-0.6.2 → linked_data_python-0.6.4}/linked_data_python.egg-info/SOURCES.txt +0 -0
  32. {linked_data_python-0.6.2 → linked_data_python-0.6.4}/linked_data_python.egg-info/dependency_links.txt +0 -0
  33. {linked_data_python-0.6.2 → linked_data_python-0.6.4}/linked_data_python.egg-info/entry_points.txt +0 -0
  34. {linked_data_python-0.6.2 → linked_data_python-0.6.4}/linked_data_python.egg-info/requires.txt +0 -0
  35. {linked_data_python-0.6.2 → linked_data_python-0.6.4}/linked_data_python.egg-info/top_level.txt +0 -0
  36. {linked_data_python-0.6.2 → linked_data_python-0.6.4}/setup.cfg +0 -0
  37. {linked_data_python-0.6.2 → linked_data_python-0.6.4}/tests/test_bench.py +0 -0
  38. {linked_data_python-0.6.2 → linked_data_python-0.6.4}/tests/test_bindings.py +0 -0
  39. {linked_data_python-0.6.2 → linked_data_python-0.6.4}/tests/test_build.py +0 -0
  40. {linked_data_python-0.6.2 → linked_data_python-0.6.4}/tests/test_call_suffix.py +0 -0
  41. {linked_data_python-0.6.2 → linked_data_python-0.6.4}/tests/test_charsets.py +0 -0
  42. {linked_data_python-0.6.2 → linked_data_python-0.6.4}/tests/test_coercion.py +0 -0
  43. {linked_data_python-0.6.2 → linked_data_python-0.6.4}/tests/test_console.py +0 -0
  44. {linked_data_python-0.6.2 → linked_data_python-0.6.4}/tests/test_debug.py +0 -0
  45. {linked_data_python-0.6.2 → linked_data_python-0.6.4}/tests/test_debug_stepping.py +0 -0
  46. {linked_data_python-0.6.2 → linked_data_python-0.6.4}/tests/test_disambiguation.py +0 -0
  47. {linked_data_python-0.6.2 → linked_data_python-0.6.4}/tests/test_docs.py +0 -0
  48. {linked_data_python-0.6.2 → linked_data_python-0.6.4}/tests/test_docstrings.py +0 -0
  49. {linked_data_python-0.6.2 → linked_data_python-0.6.4}/tests/test_examples.py +0 -0
  50. {linked_data_python-0.6.2 → linked_data_python-0.6.4}/tests/test_formatter.py +0 -0
  51. {linked_data_python-0.6.2 → linked_data_python-0.6.4}/tests/test_generator.py +0 -0
  52. {linked_data_python-0.6.2 → linked_data_python-0.6.4}/tests/test_graphs.py +0 -0
  53. {linked_data_python-0.6.2 → linked_data_python-0.6.4}/tests/test_hover_parts.py +0 -0
  54. {linked_data_python-0.6.2 → linked_data_python-0.6.4}/tests/test_identity.py +0 -0
  55. {linked_data_python-0.6.2 → linked_data_python-0.6.4}/tests/test_import_hook.py +0 -0
  56. {linked_data_python-0.6.2 → linked_data_python-0.6.4}/tests/test_islanddoc.py +0 -0
  57. {linked_data_python-0.6.2 → linked_data_python-0.6.4}/tests/test_islands_terms.py +0 -0
  58. {linked_data_python-0.6.2 → linked_data_python-0.6.4}/tests/test_lexing.py +0 -0
  59. {linked_data_python-0.6.2 → linked_data_python-0.6.4}/tests/test_linemap.py +0 -0
  60. {linked_data_python-0.6.2 → linked_data_python-0.6.4}/tests/test_lsp_rpc.py +0 -0
  61. {linked_data_python-0.6.2 → linked_data_python-0.6.4}/tests/test_lsp_server.py +0 -0
  62. {linked_data_python-0.6.2 → linked_data_python-0.6.4}/tests/test_lsp_translate.py +0 -0
  63. {linked_data_python-0.6.2 → linked_data_python-0.6.4}/tests/test_match_island.py +0 -0
  64. {linked_data_python-0.6.2 → linked_data_python-0.6.4}/tests/test_micropython_subset.py +0 -0
  65. {linked_data_python-0.6.2 → linked_data_python-0.6.4}/tests/test_prefix_import.py +0 -0
  66. {linked_data_python-0.6.2 → linked_data_python-0.6.4}/tests/test_pygments_lexer.py +0 -0
  67. {linked_data_python-0.6.2 → linked_data_python-0.6.4}/tests/test_scope_modifiers.py +0 -0
  68. {linked_data_python-0.6.2 → linked_data_python-0.6.4}/tests/test_semantics.py +0 -0
  69. {linked_data_python-0.6.2 → linked_data_python-0.6.4}/tests/test_sourcemap_v3.py +0 -0
  70. {linked_data_python-0.6.2 → linked_data_python-0.6.4}/tests/test_sparql_expr.py +0 -0
  71. {linked_data_python-0.6.2 → linked_data_python-0.6.4}/tests/test_sparql_island.py +0 -0
  72. {linked_data_python-0.6.2 → linked_data_python-0.6.4}/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.2
3
+ Version: 0.6.4
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
@@ -45,11 +45,13 @@ ISLANDS = {
45
45
  # -- declarations -------------------------------------------------------
46
46
 
47
47
  "prefix": IslandDoc(
48
- "(declaration) @prefix ex: <IRI> .",
48
+ "(declaration) @prefix ex: <IRI> [as EX] .",
49
49
  "Binds `ex:` to a namespace IRI for the rest of the enclosing block. "
50
- "A prefix is lexical: it has no run-time object, and `ex:` on its own "
51
- "is never a value. Declaring it again in a deeper block shadows it, "
52
- "the way a Python name would.",
50
+ "A prefix is lexical: `ex:` on its own is never a value. When the "
51
+ "code needs the Namespace as an OBJECT — to `bind()` it on a graph, "
52
+ "to export it, to put it in a registry — `as EX` binds that object "
53
+ "to a Python name too. Declaring the prefix again in a deeper block "
54
+ "shadows it, the way a Python name would.",
53
55
  _DECLS, "prefix-bind-a-prefix"),
54
56
 
55
57
  "base": IslandDoc(
@@ -69,7 +71,9 @@ ISLANDS = {
69
71
  "(declaration) @graph EXPR | @graph as NAME -> Graph",
70
72
  "Designates the current graph for the block — the one `+{ }`, `-{ }` "
71
73
  "and a receiver-less `m{ }` act on. `as NAME` creates a fresh graph "
72
- "and binds it to NAME; `global` and `nonlocal` widen the scope.",
74
+ "and binds it to NAME; `global` and `nonlocal` widen the scope. "
75
+ "Either way the graph inherits the block's prefixes, so it "
76
+ "serialises with them.",
73
77
  _CURRENT, "graph-designate-or-create"),
74
78
 
75
79
  "bindings-decl": IslandDoc(
@@ -254,6 +254,10 @@ _NO_CURRENT_GRAPH = re.compile(r"sans graphe courant")
254
254
  #: input from looping.
255
255
  _MAX_SYNTHETIC = 24
256
256
 
257
+ #: `as EX` closing a prefix declaration (record ldpy/027).
258
+ _PREFIX_AS_RE = re.compile(
259
+ r"(>)(\s+)(as)(\s+)([A-Za-z_]\w*)(?=\s*\.)")
260
+
257
261
  #: Namespace of the synthetic prefixes. Never resolved — the map is thrown
258
262
  #: away after the tokens are read — but it has to be a legal IRI.
259
263
  _SYNTHETIC_NS = "urn:x-ldpy-highlight:"
@@ -400,10 +404,26 @@ class LdpyLexer(Lexer):
400
404
 
401
405
  # -- the ldpy-specific regions of an island body -------------------------
402
406
 
407
+ def _emit_prefix_as(self, text, m):
408
+ """The `as EX` of a prefix declaration: a keyword and a Python name."""
409
+ def emit(offset):
410
+ yield offset + m.start(3), Keyword, m.group(3)
411
+ yield offset + m.end(3), Text, m.group(4)
412
+ yield offset + m.start(5), Name.Variable, m.group(5)
413
+ return emit
414
+
403
415
  def _regions(self, text, flavour, is_query=False):
404
416
  """Locate what no Turtle or SPARQL lexer can read: interpolations and
405
417
  nested islands. Each becomes a (start, end, emit) region."""
406
418
  regions = []
419
+ if flavour == "turtle":
420
+ # `@prefix ex: <IRI> as EX .` (record ldpy/027): Turtle's own
421
+ # lexer knows the directive but not the clause, and would mark it
422
+ # Error. Mask it and emit it ourselves.
423
+ m = _PREFIX_AS_RE.search(text)
424
+ if m:
425
+ regions.append((m.start(3), m.end(5),
426
+ self._emit_prefix_as(text, m)))
407
427
  i, n = 0, len(text)
408
428
  while i < n:
409
429
  c = text[i]
@@ -708,6 +708,30 @@ def new_graph(namespaces, base, identifier=None):
708
708
  return g
709
709
 
710
710
 
711
+ def designate(namespaces, graph):
712
+ """'@graph EXPR' — the designated graph inherits the block's prefixes.
713
+
714
+ A graph CREATED by '@graph as g' already serialises with the prefixes in
715
+ scope (:func:`new_graph`); a graph merely DESIGNATED did not, so the same
716
+ block writing the same prefixed names into two graphs produced two
717
+ different serialisations. That was an inconsistency rather than a
718
+ decision, and record ldpy/027 settles it: both inherit.
719
+
720
+ The bindings are added to the graph's OWN manager, never by swapping it
721
+ for the shared cached one — a designated graph belongs to the caller and
722
+ may already carry bindings that matter. Anything that is not a graph, or
723
+ a backend without a namespace manager, passes through untouched: the
724
+ declaration's job is to designate, and it must not fail on the shape of
725
+ what it is given.
726
+ """
727
+ if namespaces:
728
+ try:
729
+ _backend.bind_namespaces(graph, namespaces)
730
+ except (AttributeError, TypeError):
731
+ pass # not a graph, or no binding support
732
+ return graph
733
+
734
+
711
735
  def add_to(graph, *triples, bindings=None):
712
736
  """'+{ ... }': instantiate and add to the current graph (record ldpy/014).
713
737
  A triple with a term still unbound is dropped — one cannot write an
@@ -1320,6 +1320,7 @@ class Transpiler:
1320
1320
  if end is None:
1321
1321
  return False
1322
1322
  k = self._skip_ws_ahead(end)
1323
+ as_name, k = self._prefix_as_clause(k)
1323
1324
  if k >= self.n or t[k] != ".":
1324
1325
  return False
1325
1326
  # consommation effective
@@ -1348,9 +1349,40 @@ class Transpiler:
1348
1349
  scope_mode)
1349
1350
  gen = "__namespaces__[%r] = %s.Namespace(%r)" % (
1350
1351
  prefix, RUNTIME_ALIAS, resolved)
1352
+ if as_name:
1353
+ # `as EX` binds the Namespace OBJECT to a Python name, for the
1354
+ # code that needs it to survive the block (record ldpy/027).
1355
+ gen = "__namespaces__[%r] = %s = %s.Namespace(%r)" % (
1356
+ prefix, as_name, RUNTIME_ALIAS, resolved)
1357
+ if scope_mode:
1358
+ gen = "%s %s; %s" % (scope_mode, as_name, gen)
1351
1359
  self._end_island(kind, mark, gen)
1352
1360
  return True
1353
1361
 
1362
+ def _prefix_as_clause(self, k):
1363
+ """`as NAME` before a @prefix declaration's closing '.', or nothing.
1364
+
1365
+ `@prefix` is lexical and yields no Python value, which the corpus
1366
+ study showed to be its most attested limit: code that must keep the
1367
+ `Namespace` as an OBJECT — to `g.bind()` it, to export it, to put it
1368
+ in a registry — had no translation at all (record ldpy/027). `as EX`
1369
+ binds that object, without making a bare `ex:` a value (records
1370
+ ldpy/002 and 004 refuse that, and still do).
1371
+
1372
+ Returns `(name, index after the clause)`; `(None, k)` when absent.
1373
+ """
1374
+ t = self.text
1375
+ if t[k:k + 2] != "as":
1376
+ return None, k
1377
+ if k + 2 < self.n and _name_char(t[k + 2]):
1378
+ return None, k # `assets`, not `as`
1379
+ j = self._skip_ws_ahead(k + 2)
1380
+ m = _PYNAME_RE.match(t, j)
1381
+ if not m or j == k + 2:
1382
+ self._error("nom Python attendu après 'as' dans la "
1383
+ "déclaration @prefix")
1384
+ return m.group(0), self._skip_ws_ahead(m.end())
1385
+
1354
1386
  def _prefix_firi_decl(self, j, prefix, scope_mode=None):
1355
1387
  """Consomme `@prefix p: f<...> .` (self.i sur '@', j sur 'f').
1356
1388
  Sans interpolation : déclaration statique ordinaire. Avec : préfixe
@@ -1360,6 +1392,10 @@ class Transpiler:
1360
1392
  self._take(j - self.i) # '@prefix p:' + blancs
1361
1393
  parts = self._take_firi_parts()
1362
1394
  self._g_ws()
1395
+ as_name, k = self._prefix_as_clause(self.i)
1396
+ if as_name:
1397
+ self._take(k - self.i)
1398
+ self._g_ws()
1363
1399
  if self._peek() != ".":
1364
1400
  self._error("'.' attendu pour clore la déclaration @prefix")
1365
1401
  self._take(1)
@@ -1379,6 +1415,11 @@ class Transpiler:
1379
1415
  scope_mode)
1380
1416
  gen = "__namespaces__[%r] = %s.Namespace(%r)" % (
1381
1417
  prefix, RUNTIME_ALIAS, resolved)
1418
+ if as_name:
1419
+ gen = "__namespaces__[%r] = %s = %s.Namespace(%r)" % (
1420
+ prefix, as_name, RUNTIME_ALIAS, resolved)
1421
+ if scope_mode:
1422
+ gen = "%s %s; %s" % (scope_mode, as_name, gen)
1382
1423
  else:
1383
1424
  # global/nonlocal : réutiliser la variable de la liaison cible si
1384
1425
  # elle est dynamique — c'est elle que les usages du dehors lisent
@@ -1392,8 +1433,14 @@ class Transpiler:
1392
1433
  gen = "%s = %s.Namespace(%s.firi(%s)); __namespaces__[%r] = %s" % (
1393
1434
  var, RUNTIME_ALIAS, RUNTIME_ALIAS,
1394
1435
  self._firi_args(parts), prefix, var)
1436
+ if as_name:
1437
+ # `as EX` on a DYNAMIC prefix binds the Namespace built from
1438
+ # the IRI resolved at that point — the same object the
1439
+ # prefix itself uses, so the two can never disagree.
1440
+ gen += "; %s = %s" % (as_name, var)
1395
1441
  if scope_mode:
1396
- gen = "%s %s; %s" % (scope_mode, var, gen)
1442
+ names = var if not as_name else "%s, %s" % (var, as_name)
1443
+ gen = "%s %s; %s" % (scope_mode, names, gen)
1397
1444
  self._end_island("prefix", mark, gen)
1398
1445
  return True
1399
1446
 
@@ -1579,7 +1626,10 @@ class Transpiler:
1579
1626
  self._error("expression attendue après '@graph'")
1580
1627
  if "\n" in expr:
1581
1628
  self._error("l'expression de '@graph' tient sur sa ligne")
1582
- rhs = "(%s)" % expr
1629
+ # The designated graph inherits the block's prefixes, exactly
1630
+ # as a created one does (record ldpy/027).
1631
+ rhs = "%s.designate(__namespaces__, (%s))" % (
1632
+ RUNTIME_ALIAS, expr)
1583
1633
  self._graph_ws_inline()
1584
1634
  if self._peek() not in "\n\r#;" and self._peek() != "":
1585
1635
  self._error("fin de ligne attendue après la déclaration @graph")
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: linked-data-python
3
- Version: 0.6.2
3
+ Version: 0.6.4
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.2"
7
+ version = "0.6.4"
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" }
@@ -288,3 +288,46 @@ def test_compound_suite_does_not_capture_python(run):
288
288
  """))
289
289
  assert g["n"] == 5 and g["d"] == {"a": 1}
290
290
  assert g["f"]("x") == "x" and g["keys"] == {"b"}
291
+
292
+
293
+ # --------------------------------------------------------------- record 027
294
+ # A graph CREATED by `@graph as g` already carried the block's prefixes; a
295
+ # graph merely DESIGNATED carried none, so the same block writing the same
296
+ # prefixed names into two graphs serialised them differently. The corpus
297
+ # study met this as nineteen hand-written `g.bind()` calls in one function.
298
+
299
+ def test_a_designated_graph_inherits_the_block_prefixes(run):
300
+ ns, _ = run("from rdflib import Graph\n"
301
+ "@prefix ex: <http://example.org/ns#> .\n"
302
+ "mine = Graph()\n"
303
+ "@graph mine\n"
304
+ "+{ ex:a ex:p 1 }\n")
305
+ assert ("ex", URIRef("http://example.org/ns#")) in list(
306
+ ns["mine"].namespaces())
307
+
308
+
309
+ def test_created_and_designated_serialise_alike(run):
310
+ """The point of the fix: two graphs the same block fills the same way
311
+ must not differ in their serialisation."""
312
+ ns, _ = run("from rdflib import Graph\n"
313
+ "@prefix ex: <http://example.org/ns#> .\n"
314
+ "@graph as made\n"
315
+ "+{ ex:a ex:p 1 }\n"
316
+ "given = Graph()\n"
317
+ "@graph given\n"
318
+ "+{ ex:a ex:p 1 }\n")
319
+ assert "@prefix ex:" in ns["made"].serialize(format="turtle")
320
+ assert "@prefix ex:" in ns["given"].serialize(format="turtle")
321
+
322
+
323
+ def test_designating_keeps_bindings_the_caller_already_had(run):
324
+ """A designated graph belongs to its caller: the block's prefixes are
325
+ ADDED to its own manager, never swapped for a shared one."""
326
+ ns, _ = run("from rdflib import Graph, Namespace\n"
327
+ "@prefix ex: <http://example.org/ns#> .\n"
328
+ "mine = Graph()\n"
329
+ 'mine.bind("mine", Namespace("http://mine.example/"))\n'
330
+ "@graph mine\n"
331
+ "+{ ex:a ex:p 1 }\n")
332
+ got = {p for p, _ in ns["mine"].namespaces()}
333
+ assert "ex" in got and "mine" in got
@@ -300,3 +300,71 @@ def test_never_declared_prefix_gives_no_warning():
300
300
  r = transpile("d = {foo:bar}\nx = arr[i:j]\n")
301
301
  assert r.warnings == []
302
302
  assert r.code == "d = {foo:bar}\nx = arr[i:j]\n"
303
+
304
+ # --------------------------------------------------------------- record 027
305
+ # `@prefix` is lexical and yields no Python value, which the corpus study
306
+ # showed to be its most attested limit: code that must keep the Namespace as
307
+ # an OBJECT — to `g.bind()` it, to export it, to put it in a registry — had
308
+ # no translation at all. `as EX` binds that object. A bare `ex:` is still
309
+ # never a value (records ldpy/002 and 004).
310
+
311
+ def test_as_binds_the_namespace_object(run):
312
+ ns, _ = run("@prefix ex: <http://example.org/ns#> as EX .\n"
313
+ "iri = EX.Thing\n"
314
+ "prefixed = ex:Thing\n")
315
+ assert str(ns["EX"]) == "http://example.org/ns#"
316
+ assert ns["iri"] == ns["prefixed"]
317
+
318
+
319
+ def test_as_serves_the_three_forms_the_corpus_needed(run):
320
+ ns, _ = run("from rdflib import Graph\n"
321
+ "@prefix ex: <http://example.org/ns#> as EX .\n"
322
+ "g = Graph()\n"
323
+ "g.bind('ex', EX)\n"
324
+ "registry = {'ex': EX}\n"
325
+ "exported = EX\n")
326
+ assert ("ex", URIRef("http://example.org/ns#")) in list(
327
+ ns["g"].namespaces())
328
+ assert ns["registry"]["ex"] is ns["exported"]
329
+
330
+
331
+ def test_the_prefix_still_works_without_as(run):
332
+ ns, _ = run("@prefix ex: <http://example.org/ns#> .\n"
333
+ "t = ex:Thing\n")
334
+ assert str(ns["t"]) == "http://example.org/ns#Thing"
335
+ assert "EX" not in ns
336
+
337
+
338
+ def test_as_needs_a_name():
339
+ with pytest.raises(LdpySyntaxError) as e:
340
+ transpile("@prefix ex: <http://example.org/> as .\n", "t.ldpy")
341
+ assert "as" in e.value.msg
342
+
343
+
344
+ def test_a_prefix_named_as_something_is_not_an_as_clause(run):
345
+ """`as` must be a whole word: a declaration is not broken by an IRI
346
+ followed by nothing, nor by a name that merely starts with 'as'."""
347
+ ns, _ = run("@prefix ex: <http://example.org/ns#> as assets .\n"
348
+ "t = ex:Thing\n")
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#"