linked-data-python 0.5.1__tar.gz → 0.5.2__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 (70) hide show
  1. {linked_data_python-0.5.1/linked_data_python.egg-info → linked_data_python-0.5.2}/PKG-INFO +1 -1
  2. {linked_data_python-0.5.1 → linked_data_python-0.5.2}/ldpy/console.py +13 -0
  3. {linked_data_python-0.5.1 → linked_data_python-0.5.2}/ldpy/debug.py +24 -2
  4. {linked_data_python-0.5.1 → linked_data_python-0.5.2}/ldpy/runtime.py +1 -3
  5. {linked_data_python-0.5.1 → linked_data_python-0.5.2}/ldpy/transpiler/core.py +12 -8
  6. {linked_data_python-0.5.1 → linked_data_python-0.5.2/linked_data_python.egg-info}/PKG-INFO +1 -1
  7. {linked_data_python-0.5.1 → linked_data_python-0.5.2}/pyproject.toml +1 -1
  8. {linked_data_python-0.5.1 → linked_data_python-0.5.2}/tests/test_console.py +58 -0
  9. {linked_data_python-0.5.1 → linked_data_python-0.5.2}/tests/test_debug.py +41 -0
  10. linked_data_python-0.5.2/tests/test_docs.py +103 -0
  11. linked_data_python-0.5.1/tests/test_docs.py +0 -46
  12. {linked_data_python-0.5.1 → linked_data_python-0.5.2}/LICENSE.md +0 -0
  13. {linked_data_python-0.5.1 → linked_data_python-0.5.2}/README.md +0 -0
  14. {linked_data_python-0.5.1 → linked_data_python-0.5.2}/ldpy/__init__.py +0 -0
  15. {linked_data_python-0.5.1 → linked_data_python-0.5.2}/ldpy/__main__.py +0 -0
  16. {linked_data_python-0.5.1 → linked_data_python-0.5.2}/ldpy/build.py +0 -0
  17. {linked_data_python-0.5.1 → linked_data_python-0.5.2}/ldpy/formatter.py +0 -0
  18. {linked_data_python-0.5.1 → linked_data_python-0.5.2}/ldpy/importer.py +0 -0
  19. {linked_data_python-0.5.1 → linked_data_python-0.5.2}/ldpy/lsp/__init__.py +0 -0
  20. {linked_data_python-0.5.1 → linked_data_python-0.5.2}/ldpy/lsp/__main__.py +0 -0
  21. {linked_data_python-0.5.1 → linked_data_python-0.5.2}/ldpy/lsp/backend.py +0 -0
  22. {linked_data_python-0.5.1 → linked_data_python-0.5.2}/ldpy/lsp/hover.py +0 -0
  23. {linked_data_python-0.5.1 → linked_data_python-0.5.2}/ldpy/lsp/islanddoc.py +0 -0
  24. {linked_data_python-0.5.1 → linked_data_python-0.5.2}/ldpy/lsp/rpc.py +0 -0
  25. {linked_data_python-0.5.1 → linked_data_python-0.5.2}/ldpy/lsp/server.py +0 -0
  26. {linked_data_python-0.5.1 → linked_data_python-0.5.2}/ldpy/lsp/translate.py +0 -0
  27. {linked_data_python-0.5.1 → linked_data_python-0.5.2}/ldpy/pygments_lexer.py +0 -0
  28. {linked_data_python-0.5.1 → linked_data_python-0.5.2}/ldpy/sparql.py +0 -0
  29. {linked_data_python-0.5.1 → linked_data_python-0.5.2}/ldpy/transpiler/__init__.py +0 -0
  30. {linked_data_python-0.5.1 → linked_data_python-0.5.2}/ldpy/transpiler/errors.py +0 -0
  31. {linked_data_python-0.5.1 → linked_data_python-0.5.2}/ldpy/transpiler/linemap.py +0 -0
  32. {linked_data_python-0.5.1 → linked_data_python-0.5.2}/linked_data_python.egg-info/SOURCES.txt +0 -0
  33. {linked_data_python-0.5.1 → linked_data_python-0.5.2}/linked_data_python.egg-info/dependency_links.txt +0 -0
  34. {linked_data_python-0.5.1 → linked_data_python-0.5.2}/linked_data_python.egg-info/entry_points.txt +0 -0
  35. {linked_data_python-0.5.1 → linked_data_python-0.5.2}/linked_data_python.egg-info/requires.txt +0 -0
  36. {linked_data_python-0.5.1 → linked_data_python-0.5.2}/linked_data_python.egg-info/top_level.txt +0 -0
  37. {linked_data_python-0.5.1 → linked_data_python-0.5.2}/setup.cfg +0 -0
  38. {linked_data_python-0.5.1 → linked_data_python-0.5.2}/tests/test_bench.py +0 -0
  39. {linked_data_python-0.5.1 → linked_data_python-0.5.2}/tests/test_bindings.py +0 -0
  40. {linked_data_python-0.5.1 → linked_data_python-0.5.2}/tests/test_build.py +0 -0
  41. {linked_data_python-0.5.1 → linked_data_python-0.5.2}/tests/test_call_suffix.py +0 -0
  42. {linked_data_python-0.5.1 → linked_data_python-0.5.2}/tests/test_charsets.py +0 -0
  43. {linked_data_python-0.5.1 → linked_data_python-0.5.2}/tests/test_coercion.py +0 -0
  44. {linked_data_python-0.5.1 → linked_data_python-0.5.2}/tests/test_current_graph.py +0 -0
  45. {linked_data_python-0.5.1 → linked_data_python-0.5.2}/tests/test_debug_stepping.py +0 -0
  46. {linked_data_python-0.5.1 → linked_data_python-0.5.2}/tests/test_disambiguation.py +0 -0
  47. {linked_data_python-0.5.1 → linked_data_python-0.5.2}/tests/test_docstrings.py +0 -0
  48. {linked_data_python-0.5.1 → linked_data_python-0.5.2}/tests/test_examples.py +0 -0
  49. {linked_data_python-0.5.1 → linked_data_python-0.5.2}/tests/test_formatter.py +0 -0
  50. {linked_data_python-0.5.1 → linked_data_python-0.5.2}/tests/test_generator.py +0 -0
  51. {linked_data_python-0.5.1 → linked_data_python-0.5.2}/tests/test_graphs.py +0 -0
  52. {linked_data_python-0.5.1 → linked_data_python-0.5.2}/tests/test_hover_parts.py +0 -0
  53. {linked_data_python-0.5.1 → linked_data_python-0.5.2}/tests/test_identity.py +0 -0
  54. {linked_data_python-0.5.1 → linked_data_python-0.5.2}/tests/test_import_hook.py +0 -0
  55. {linked_data_python-0.5.1 → linked_data_python-0.5.2}/tests/test_islanddoc.py +0 -0
  56. {linked_data_python-0.5.1 → linked_data_python-0.5.2}/tests/test_islands_terms.py +0 -0
  57. {linked_data_python-0.5.1 → linked_data_python-0.5.2}/tests/test_linemap.py +0 -0
  58. {linked_data_python-0.5.1 → linked_data_python-0.5.2}/tests/test_lsp_rpc.py +0 -0
  59. {linked_data_python-0.5.1 → linked_data_python-0.5.2}/tests/test_lsp_server.py +0 -0
  60. {linked_data_python-0.5.1 → linked_data_python-0.5.2}/tests/test_lsp_translate.py +0 -0
  61. {linked_data_python-0.5.1 → linked_data_python-0.5.2}/tests/test_match_island.py +0 -0
  62. {linked_data_python-0.5.1 → linked_data_python-0.5.2}/tests/test_micropython_subset.py +0 -0
  63. {linked_data_python-0.5.1 → linked_data_python-0.5.2}/tests/test_prefix_import.py +0 -0
  64. {linked_data_python-0.5.1 → linked_data_python-0.5.2}/tests/test_prefix_scoping.py +0 -0
  65. {linked_data_python-0.5.1 → linked_data_python-0.5.2}/tests/test_pygments_lexer.py +0 -0
  66. {linked_data_python-0.5.1 → linked_data_python-0.5.2}/tests/test_scope_modifiers.py +0 -0
  67. {linked_data_python-0.5.1 → linked_data_python-0.5.2}/tests/test_semantics.py +0 -0
  68. {linked_data_python-0.5.1 → linked_data_python-0.5.2}/tests/test_sourcemap_v3.py +0 -0
  69. {linked_data_python-0.5.1 → linked_data_python-0.5.2}/tests/test_sparql_expr.py +0 -0
  70. {linked_data_python-0.5.1 → linked_data_python-0.5.2}/tests/test_sparql_island.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: linked-data-python
3
- Version: 0.5.1
3
+ Version: 0.5.2
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
@@ -70,6 +70,12 @@ class LdpyConsole(code.InteractiveConsole):
70
70
  self._prefixes = dict(prefixes or {})
71
71
  self._prefix_cols = {k: 0 for k in self._prefixes}
72
72
  self._base = base
73
+ # The current graph and the current bindings are declarations too:
74
+ # like @prefix, a top-level @graph must survive to the next entry
75
+ # (otherwise `+{ ... }` after `@graph as g` sees no graph at all).
76
+ self._graph_var = None
77
+ self._bindings_var = None
78
+ self._counter = 0
73
79
 
74
80
  def runsource(self, source, filename=None, symbol="single"):
75
81
  """Transpile then compile one entry; True = incomplete entry
@@ -79,6 +85,10 @@ class LdpyConsole(code.InteractiveConsole):
79
85
  t.prefixes = dict(self._prefixes)
80
86
  t._prefix_col = dict(self._prefix_cols)
81
87
  t.base = self._base
88
+ t._graph_var = self._graph_var
89
+ t._bindings_var = self._bindings_var
90
+ # fresh names must not collide with those of the previous entries
91
+ t._ns_counter = self._counter
82
92
  try:
83
93
  result = t.run()
84
94
  except LdpySyntaxError as e:
@@ -98,6 +108,9 @@ class LdpyConsole(code.InteractiveConsole):
98
108
  self._prefixes = dict(t.prefixes)
99
109
  self._prefix_cols = dict(t._prefix_col)
100
110
  self._base = t.base
111
+ self._graph_var = t._graph_var
112
+ self._bindings_var = t._bindings_var
113
+ self._counter = t._ns_counter
101
114
  for w in result.warnings:
102
115
  self.write(str(w) + "\n")
103
116
  self.runcode(code_obj)
@@ -92,11 +92,30 @@ def probe():
92
92
  #: re-exported: debug tooling looks for them here (pure functions, defined
93
93
  #: alongside the map in ldpy/transpiler/linemap.py)
94
94
  __all__ = ["stepping_rules", "probe", "package_dir", "load_map",
95
+ "shadow_rel",
95
96
  "translate_breakpoints", "translate_frames",
96
97
  "snap_breakpoint_line", "snap_breakpoint_lines",
97
98
  "run_direct", "main"]
98
99
 
99
100
 
101
+ def shadow_rel(source, root=None):
102
+ """Path of the shadow inside the output directory.
103
+
104
+ Without a root, the basename: the shadow sits beside the source, and two
105
+ files never meet. With one — a workspace whose whole build is gathered in
106
+ a single directory — the tree is mirrored, which is what keeps `a/m.ldpy`
107
+ and `b/m.ldpy` from writing the same `m.py`. A source outside the root
108
+ falls back to its basename rather than climbing out of the output
109
+ directory with `../`."""
110
+ name = os.path.basename(source)
111
+ if not root:
112
+ return name
113
+ rel = os.path.relpath(os.path.abspath(source), os.path.abspath(root))
114
+ if rel.startswith(os.pardir + os.sep) or rel == os.pardir:
115
+ return name
116
+ return rel
117
+
118
+
100
119
  def load_map(map_path):
101
120
  """Load a LanguageMap from a .ldpy.map file (JSON)."""
102
121
  with open(map_path, "r", encoding="utf-8") as f:
@@ -167,6 +186,10 @@ def main(argv=None):
167
186
  "straight in the .ldpy)")
168
187
  parser.add_argument("-o", "--out", default=DEFAULT_OUT,
169
188
  help="shadow directory (default: %(default)s)")
189
+ parser.add_argument("--root", metavar="DIR",
190
+ help="tree root the shadow mirrors: the .py is "
191
+ "written at OUT/<path of the source relative "
192
+ "to DIR> (default: OUT/<basename>)")
170
193
  parser.add_argument("--listen", metavar="HOTE:PORT",
171
194
  help="start debugpy listening (e.g. 127.0.0.1:5678)")
172
195
  parser.add_argument("--wait-for-client", action="store_true",
@@ -201,8 +224,7 @@ def main(argv=None):
201
224
 
202
225
  try:
203
226
  py_path, map_path, result = build_file(
204
- args.source, args.out,
205
- rel=os.path.basename(args.source))
227
+ args.source, args.out, rel=shadow_rel(args.source, args.root))
206
228
  except LdpySyntaxError as e:
207
229
  print(str(e), file=sys.stderr)
208
230
  return 1
@@ -641,7 +641,6 @@ def add_to(graph, *triples, bindings=None):
641
641
  s2, p2, o2 = term(s), term(p), term(o)
642
642
  if s2 is not None and p2 is not None and o2 is not None:
643
643
  graph.add((s2, p2, o2))
644
- return graph
645
644
 
646
645
 
647
646
  def remove_from(graph, *patterns, bindings=None):
@@ -665,13 +664,12 @@ def remove_from(graph, *patterns, bindings=None):
665
664
  to_remove.add(inst)
666
665
  for tr in to_remove:
667
666
  graph.remove(tr)
668
- return graph
667
+ return
669
668
  term = _materializer(bindings, keep_vars=True)
670
669
  for s, p, o in patterns:
671
670
  tr = tuple(None if isinstance(x, (Variable, bn)) else x
672
671
  for x in (term(s), term(p), term(o)))
673
672
  graph.remove(tr)
674
- return graph
675
673
 
676
674
 
677
675
  def graph(namespaces, base, *triples, bindings=None):
@@ -345,13 +345,16 @@ class Transpiler:
345
345
  self.operand = False
346
346
  self.stmt_start = False
347
347
 
348
- def _error(self, msg, line=None, col=None):
348
+ def _error(self, msg, line=None, col=None, incomplete=True):
349
349
  exc = LdpySyntaxError(msg, self.filename,
350
350
  self.src_line if line is None else line,
351
351
  self.src_col if col is None else col)
352
352
  # la console s'en sert pour distinguer « entrée incomplète » (îlot
353
- # non fermé en fin de tampon) d'une vraie erreur de syntaxe
354
- exc.at_eof = self.i >= self.n
353
+ # non fermé en fin de tampon) d'une vraie erreur de syntaxe.
354
+ # `incomplete=False` : erreur qu'une suite d'entrée ne répare pas
355
+ # (elle porte sur ce qui a déjà été lu), même si le curseur est en
356
+ # fin de tampon — sinon la console attendrait indéfiniment.
357
+ exc.at_eof = incomplete and self.i >= self.n
355
358
  raise exc
356
359
 
357
360
  def _warn(self, msg):
@@ -675,7 +678,7 @@ class Transpiler:
675
678
  if re.match(r"@(prefix|base|graph|bindings)\b", t[j:j + 10]):
676
679
  self._close_copy()
677
680
  self._take(m.end() - self.i) # le mot-clé
678
- while self._peek() in " \t":
681
+ while self._peek() in (" ", "\t"):
679
682
  self._take(1)
680
683
  if not self._try_prefix_or_base(scope_mode=name):
681
684
  self._error("déclaration d'îlot attendue après "
@@ -1388,7 +1391,8 @@ class Transpiler:
1388
1391
  if not encl:
1389
1392
  what = "prefix %s:" % name if kind == "prefix" else kind
1390
1393
  self._error("nonlocal @%s : aucune déclaration englobante "
1391
- "(comme le nonlocal de Python)" % what)
1394
+ "(comme le nonlocal de Python)" % what,
1395
+ incomplete=False)
1392
1396
  keep = encl[-1]
1393
1397
  target_col = self._scope_stack[keep][0]
1394
1398
  touched = [i for i in matches if i > keep]
@@ -1562,7 +1566,7 @@ class Transpiler:
1562
1566
  return self._take(m.end() - self.i)
1563
1567
 
1564
1568
  def _graph_ws_inline(self):
1565
- while self._peek() in " \t":
1569
+ while self._peek() in (" ", "\t"):
1566
1570
  self._take(1)
1567
1571
 
1568
1572
  def _addremove_island(self, sign):
@@ -1581,7 +1585,7 @@ class Transpiler:
1581
1585
  self._error("'%s{ ... }' sans graphe courant : déclarez-le avec "
1582
1586
  "'@graph <expression>' ou '@graph as g' (fiche 014), "
1583
1587
  "ou donnez-le en suffixe — %s{ ... }(g)"
1584
- % (sign, sign))
1588
+ % (sign, sign), incomplete=False)
1585
1589
  bvar = sfx_bindings if sfx_bindings else self._bindings_var
1586
1590
  fn = "add_to" if sign == "+" else "remove_from"
1587
1591
  args = [gvar]
@@ -2482,7 +2486,7 @@ class Transpiler:
2482
2486
  if word.upper() == "BOUND" and after == "(":
2483
2487
  self._take(len(word) + 1)
2484
2488
  self._e_ws()
2485
- if self._peek() not in "?$":
2489
+ if self._peek() not in ("?", "$"):
2486
2490
  self._error("BOUND attend une variable ?v")
2487
2491
  self._take(1)
2488
2492
  vm = _NAME_RE.match(t, self.i)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: linked-data-python
3
- Version: 0.5.1
3
+ Version: 0.5.2
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.5.1"
7
+ version = "0.5.2"
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" }
@@ -94,3 +94,61 @@ def test_interactive_after_script(tmp_path):
94
94
  p = console("X\nex:b\n", args=("-i", str(script)))
95
95
  assert "URIRef('http://e/a')" in p.stdout # les globals du script
96
96
  assert "URIRef('http://e/b')" in p.stdout # et ses préfixes
97
+
98
+
99
+ def test_current_graph_persists_across_entries():
100
+ """`@graph as g` then `+{ }` on the next line: the current graph is a
101
+ declaration like @prefix, it must survive the entry that declares it."""
102
+ p = console("@prefix ex: <http://e/> .\n"
103
+ "@graph as g\n"
104
+ "+{ ex:alice a ex:Person }\n"
105
+ "len(g)\n")
106
+ assert p.returncode == 0
107
+ assert "1" in results(p)
108
+
109
+
110
+ def test_add_island_prints_nothing():
111
+ """`+{ }` is a statement: the console must not echo the graph."""
112
+ p = console("@prefix ex: <http://e/> .\n"
113
+ "@graph as g\n"
114
+ "+{ ex:alice a ex:Person }\n")
115
+ assert "Graph" not in p.stdout
116
+
117
+
118
+ def test_current_bindings_persist_across_entries():
119
+ """`for @bindings as b in ...` names the bindings; the entries that
120
+ follow must still see the current graph declared before them."""
121
+ p = console("@prefix ex: <http://e/> .\n"
122
+ "@graph as g\n"
123
+ "for @bindings as b in [{\"n\": \"ana\"}]:\n"
124
+ " +{ ex:p ex:name ?n }\n"
125
+ "\n"
126
+ "len(g)\n")
127
+ assert "1" in results(p)
128
+
129
+
130
+ def test_add_without_current_graph_reports_at_once():
131
+ """An island that is complete but has no current graph is an error, not
132
+ an entry waiting to be finished: the console used to hang on it."""
133
+ p = console("@prefix ex: <http://e/> .\n"
134
+ "+{ ex:a ex:b ex:c }\n"
135
+ "'survivant'\n")
136
+ assert p.returncode == 0
137
+ assert "graphe courant" in p.stderr
138
+ assert "survivant" in p.stdout
139
+
140
+
141
+ def test_graph_declaration_at_end_of_buffer():
142
+ """`@graph as g` with no trailing newline: the inline-whitespace scan
143
+ used to loop for ever on the empty string returned at end of text."""
144
+ from ldpy.transpiler.core import Transpiler
145
+ t = Transpiler("@graph as g", "<c>", emit_prelude=False)
146
+ assert t.run().code.startswith("g = ")
147
+
148
+
149
+ def test_bound_at_end_of_buffer_is_an_error_not_a_crash():
150
+ from ldpy.transpiler import LdpySyntaxError
151
+ from ldpy.transpiler.core import Transpiler
152
+ import pytest
153
+ with pytest.raises(LdpySyntaxError):
154
+ Transpiler("e{ BOUND(", "<c>", emit_prelude=False).run()
@@ -170,3 +170,44 @@ def test_cli_under_real_debugpy(tmp_path):
170
170
  p = run_debug(tmp_path, ["--listen", "127.0.0.1:0"])
171
171
  assert p.returncode == 0, p.stderr
172
172
  assert p.stdout.strip().endswith("2")
173
+
174
+
175
+ # ------------------------------------------------- où le .py est matérialisé
176
+
177
+ def test_shadow_rel_without_root_is_the_basename():
178
+ from ldpy.debug import shadow_rel
179
+ assert shadow_rel("/w/a/m.ldpy") == "m.ldpy"
180
+
181
+
182
+ def test_shadow_rel_mirrors_the_tree_under_the_root():
183
+ from ldpy.debug import shadow_rel
184
+ assert shadow_rel(os.path.join("/w", "a", "m.ldpy"), "/w") == \
185
+ os.path.join("a", "m.ldpy")
186
+
187
+
188
+ def test_shadow_rel_never_climbs_out_of_the_output_directory():
189
+ """A source outside the root would give '../…': the shadow would land
190
+ beside the build directory instead of inside it."""
191
+ from ldpy.debug import shadow_rel
192
+ assert shadow_rel("/elsewhere/m.ldpy", "/w") == "m.ldpy"
193
+
194
+
195
+ def test_cli_root_keeps_same_named_files_apart(tmp_path):
196
+ """One build directory for the whole workspace: `a/m.ldpy` and
197
+ `b/m.ldpy` must not both write `m.py`."""
198
+ out = tmp_path / "build"
199
+ shadows = []
200
+ for folder in ("a", "b"):
201
+ d = tmp_path / folder
202
+ d.mkdir()
203
+ (d / "m.ldpy").write_text(SRC)
204
+ env = dict(os.environ, PYTHONPATH=REPO)
205
+ p = subprocess.run(
206
+ [sys.executable, "-m", "ldpy.debug", str(d / "m.ldpy"),
207
+ "-o", str(out), "--root", str(tmp_path), "--breakpoints", "2"],
208
+ capture_output=True, text=True, cwd=REPO, env=env, timeout=120)
209
+ assert p.returncode == 0, p.stderr
210
+ shadows.append(json.loads(p.stdout)["shadow"])
211
+ assert len(set(shadows)) == 2
212
+ assert all(os.path.isfile(s) for s in shadows)
213
+ assert os.path.basename(os.path.dirname(shadows[0])) == "a"
@@ -0,0 +1,103 @@
1
+ """La documentation est exécutable : chaque bloc ``ldpy`` des pages de docs/
2
+ est transpilé PUIS EXÉCUTÉ ; chaque bloc ``python`` est exécuté. Un snippet
3
+ qui ne tourne pas ne peut pas rester dans la doc."""
4
+
5
+ import glob
6
+ import os
7
+ import re
8
+
9
+ import pytest
10
+
11
+ REPO = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
12
+ DOCS = sorted(glob.glob(os.path.join(REPO, "docs", "**", "*.md"),
13
+ recursive=True))
14
+
15
+ FENCE = re.compile(r"^```(\w+)\n(.*?)^```", re.M | re.S)
16
+
17
+
18
+ def blocks(lang):
19
+ out = []
20
+ for path in DOCS:
21
+ text = open(path, encoding="utf-8").read()
22
+ for i, m in enumerate(FENCE.finditer(text)):
23
+ if m.group(1) == lang:
24
+ out.append(pytest.param(
25
+ m.group(2),
26
+ id="%s#%d" % (os.path.relpath(path, REPO), i)))
27
+ return out
28
+
29
+
30
+ def test_docs_exist_and_cover_diataxis():
31
+ rels = {os.path.relpath(p, os.path.join(REPO, "docs")) for p in DOCS}
32
+ assert "README.md" in rels
33
+ for quadrant in ("tutorials", "how-to", "reference", "explanation"):
34
+ assert any(r.startswith(quadrant + os.sep) for r in rels), quadrant
35
+
36
+
37
+ @pytest.mark.parametrize("code", blocks("ldpy"))
38
+ def test_ldpy_snippets_run(code):
39
+ from ldpy.transpiler import transpile
40
+ result = transpile(code, "<docs>")
41
+ exec(compile(result.code, "<docs>", "exec"), {"__name__": "docs"})
42
+
43
+
44
+ @pytest.mark.parametrize("code", blocks("python"))
45
+ def test_python_snippets_run(code):
46
+ exec(compile(code, "<docs>", "exec"), {"__name__": "docs"})
47
+
48
+
49
+ # --------------------------------------------------------------------------
50
+ # The same snippets, typed into the interactive console.
51
+ #
52
+ # The console transpiles ONE ENTRY AT A TIME: what a file keeps in a single
53
+ # pass (the current @prefix, @graph, @bindings) has to survive from one entry
54
+ # to the next. A doc snippet that runs as a file but not in the console is a
55
+ # console bug, not a doc bug — hence this second pass over the same blocks.
56
+ # --------------------------------------------------------------------------
57
+
58
+ def _pasteable(code):
59
+ """A basic Python console (`code.InteractiveConsole`, what we build on)
60
+ ends a block on a blank line and has no bracketed paste. Blocks that
61
+ depend on either are out of scope here — CPython's own REPL is no
62
+ better."""
63
+ if '"""' in code or "'''" in code:
64
+ return False
65
+ lines = code.splitlines()
66
+ for i, line in enumerate(lines):
67
+ if line.strip():
68
+ continue
69
+ following = next((x for x in lines[i + 1:] if x.strip()), "")
70
+ if following.startswith((" ", "\t")):
71
+ return False # blank line inside a block
72
+ return True
73
+
74
+
75
+ def _console_output(code):
76
+ """Feed a snippet line by line, as one would type it; return everything
77
+ the console reported (errors and warnings)."""
78
+ import contextlib
79
+ import io
80
+ from ldpy.console import LdpyConsole
81
+
82
+ console = LdpyConsole()
83
+ reported = io.StringIO()
84
+ console.write = reported.write
85
+ lines = code.splitlines() + [""]
86
+ for line in lines:
87
+ # dedenting to column 0 closes the block above, as typing does
88
+ if console.buffer and line[:1] not in (" ", "\t", ""):
89
+ console.push("")
90
+ with contextlib.redirect_stdout(io.StringIO()), \
91
+ contextlib.redirect_stderr(reported):
92
+ console.push(line)
93
+ return reported.getvalue()
94
+
95
+
96
+ @pytest.mark.parametrize("code", blocks("ldpy"))
97
+ def test_ldpy_snippets_run_in_the_console(code):
98
+ if not _pasteable(code):
99
+ pytest.skip("block relies on paste, not on typing")
100
+ reported = _console_output(code)
101
+ errors = [ligne for ligne in reported.splitlines()
102
+ if ligne.strip() and "LdpyWarning" not in ligne]
103
+ assert not errors, "\n".join(errors)
@@ -1,46 +0,0 @@
1
- """La documentation est exécutable : chaque bloc ``ldpy`` des pages de docs/
2
- est transpilé PUIS EXÉCUTÉ ; chaque bloc ``python`` est exécuté. Un snippet
3
- qui ne tourne pas ne peut pas rester dans la doc."""
4
-
5
- import glob
6
- import os
7
- import re
8
-
9
- import pytest
10
-
11
- REPO = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
12
- DOCS = sorted(glob.glob(os.path.join(REPO, "docs", "**", "*.md"),
13
- recursive=True))
14
-
15
- FENCE = re.compile(r"^```(\w+)\n(.*?)^```", re.M | re.S)
16
-
17
-
18
- def blocks(lang):
19
- out = []
20
- for path in DOCS:
21
- text = open(path, encoding="utf-8").read()
22
- for i, m in enumerate(FENCE.finditer(text)):
23
- if m.group(1) == lang:
24
- out.append(pytest.param(
25
- m.group(2),
26
- id="%s#%d" % (os.path.relpath(path, REPO), i)))
27
- return out
28
-
29
-
30
- def test_docs_exist_and_cover_diataxis():
31
- rels = {os.path.relpath(p, os.path.join(REPO, "docs")) for p in DOCS}
32
- assert "README.md" in rels
33
- for quadrant in ("tutorials", "how-to", "reference", "explanation"):
34
- assert any(r.startswith(quadrant + os.sep) for r in rels), quadrant
35
-
36
-
37
- @pytest.mark.parametrize("code", blocks("ldpy"))
38
- def test_ldpy_snippets_run(code):
39
- from ldpy.transpiler import transpile
40
- result = transpile(code, "<docs>")
41
- exec(compile(result.code, "<docs>", "exec"), {"__name__": "docs"})
42
-
43
-
44
- @pytest.mark.parametrize("code", blocks("python"))
45
- def test_python_snippets_run(code):
46
- exec(compile(code, "<docs>", "exec"), {"__name__": "docs"})